├── LICENSE ├── README.md ├── android ├── .gitignore ├── .idea │ ├── codeStyles │ │ └── Project.xml │ └── runConfigurations.xml ├── README.md ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── fr │ │ │ └── fbernard │ │ │ └── newsapp │ │ │ └── android │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── fr │ │ │ │ └── fbernard │ │ │ │ └── newsapp │ │ │ │ └── android │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── NewsAdapter.kt │ │ │ │ └── data │ │ │ │ └── NewsDataService.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_money.xml │ │ │ ├── ic_sport.xml │ │ │ └── ic_tech.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── item_news.xml │ │ │ ├── menu │ │ │ └── navigation.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 │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── fr │ │ └── fbernard │ │ └── newsapp │ │ └── android │ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── backends └── kotlin-graal │ ├── .gitignore │ ├── README.md │ ├── build.gradle │ ├── graal-reflection.json │ ├── gradle.properties │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ └── main │ └── kotlin │ └── fr │ └── fbernard │ └── newsapp │ └── backend │ ├── NewsAppServer.kt │ ├── NewsService.kt │ └── data │ └── NewsStore.kt ├── docs ├── android_news.png ├── android_news_added.png ├── grpc-fullstack-architecture.svg ├── ios_news.png ├── ios_news_added.png ├── web_news.png └── web_news_added.png ├── ios ├── .gitignore ├── Podfile ├── Podfile.lock ├── README.md ├── ios.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── ios │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── economy.imageset │ │ ├── Contents.json │ │ ├── money-1.png │ │ └── money.png │ ├── sport.imageset │ │ ├── Contents.json │ │ ├── sport-1.png │ │ └── sport.png │ └── tech.imageset │ │ ├── Contents.json │ │ ├── tech-1.png │ │ └── tech.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── NewsDataService.swift │ ├── NewsTableViewCell.swift │ ├── ViewController.swift │ ├── news_service.grpc.swift │ └── news_service.pb.swift ├── newscli ├── README.md ├── client_news.go └── news_service │ └── news_service.pb.go ├── protos └── news_service.proto └── web ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── assets └── README.md ├── components ├── Logo.vue ├── News.vue └── README.md ├── layouts ├── README.md └── default.vue ├── middleware └── README.md ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages ├── README.md └── index.vue ├── plugins └── README.md ├── server └── index.js ├── static ├── README.md └── favicon.ico └── store ├── README.md ├── grpc ├── news_service_grpc_web_pb.js └── news_service_pb.js └── news.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Florian Bernard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC Full Stack example 2 | 3 | ## Overview : "One proto file to rule them all" 4 | 5 | 6 | 7 | ## Intro 8 | 9 | this example shows how from a single proto file it is possible to easily create multiple clients and servers implementations in differents languages. 10 | 11 | ## Servers 12 | 13 | * [Kotlin](backends/kotlin-graal) 14 | 15 | ## Clients 16 | 17 | * [Android Application](android/) 18 | * [Go Command line client](newscli/) 19 | * [iOS Application](ios/) 20 | * [Web Application](web/) 21 | 22 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/android,androidstudio 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the ART/Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | out/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | .idea/caches 47 | 48 | # Keystore files 49 | # Uncomment the following line if you do not want to check your keystore files in. 50 | #*.jks 51 | 52 | # External native build folder generated in Android Studio 2.2 and later 53 | .externalNativeBuild 54 | 55 | # Google Services (e.g. APIs or Firebase) 56 | google-services.json 57 | 58 | # Freeline 59 | freeline.py 60 | freeline/ 61 | freeline_project_description.json 62 | 63 | # fastlane 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | fastlane/readme.md 69 | 70 | ### Android Patch ### 71 | gen-external-apklibs 72 | 73 | ### AndroidStudio ### 74 | # Covers files to be ignored for android development using Android Studio. 75 | 76 | # Built application files 77 | 78 | # Files for the ART/Dalvik VM 79 | 80 | # Java class files 81 | 82 | # Generated files 83 | 84 | # Gradle files 85 | .gradle 86 | 87 | # Signing files 88 | .signing/ 89 | 90 | # Local configuration file (sdk path, etc) 91 | 92 | # Proguard folder generated by Eclipse 93 | 94 | # Log Files 95 | 96 | # Android Studio 97 | /*/build/ 98 | /*/local.properties 99 | /*/out 100 | /*/*/build 101 | /*/*/production 102 | *.ipr 103 | *~ 104 | *.swp 105 | 106 | # Android Patch 107 | 108 | # External native build folder generated in Android Studio 2.2 and later 109 | 110 | # NDK 111 | obj/ 112 | 113 | # IntelliJ IDEA 114 | *.iws 115 | /out/ 116 | 117 | # User-specific configurations 118 | .idea/caches/ 119 | .idea/libraries/ 120 | .idea/shelf/ 121 | .idea/.name 122 | .idea/compiler.xml 123 | .idea/copyright/profiles_settings.xml 124 | .idea/encodings.xml 125 | .idea/misc.xml 126 | .idea/modules.xml 127 | .idea/scopes/scope_settings.xml 128 | .idea/vcs.xml 129 | .idea/jsLibraryMappings.xml 130 | .idea/datasources.xml 131 | .idea/dataSources.ids 132 | .idea/sqlDataSources.xml 133 | .idea/dynamic.xml 134 | .idea/uiDesigner.xml 135 | 136 | # OS-specific files 137 | .DS_Store 138 | .DS_Store? 139 | ._* 140 | .Spotlight-V100 141 | .Trashes 142 | ehthumbs.db 143 | Thumbs.db 144 | 145 | # Legacy Eclipse project files 146 | .classpath 147 | .project 148 | .cproject 149 | .settings/ 150 | 151 | # Mobile Tools for Java (J2ME) 152 | .mtj.tmp/ 153 | 154 | # Package Files # 155 | *.war 156 | *.ear 157 | 158 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 159 | hs_err_pid* 160 | 161 | ## Plugin-specific files: 162 | 163 | # mpeltonen/sbt-idea plugin 164 | .idea_modules/ 165 | 166 | # JIRA plugin 167 | atlassian-ide-plugin.xml 168 | 169 | # Mongo Explorer plugin 170 | .idea/mongoSettings.xml 171 | 172 | # Crashlytics plugin (for Android Studio and IntelliJ) 173 | com_crashlytics_export_strings.xml 174 | crashlytics.properties 175 | crashlytics-build.properties 176 | fabric.properties 177 | 178 | ### AndroidStudio Patch ### 179 | 180 | !/gradle/wrapper/gradle-wrapper.jar 181 | 182 | 183 | # End of https://www.gitignore.io/api/android,androidstudio -------------------------------------------------------------------------------- /android/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /android/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /android/README.md: -------------------------------------------------------------------------------- 1 | # gRPC News Android client 2 | 3 | ## Overview 4 | 5 | 6 | 7 | ## Build & Run Project 8 | 9 | Use Android Studio 3.2+ to build and run application: 10 | 11 | ## Libraries used 12 | 13 | * [gRPC Java](https://github.com/grpc/grpc-java) 14 | * [Salesforce rxjava gRPC plugin ](https://github.com/salesforce/reactive-grpc/tree/master/rx-java) 15 | -------------------------------------------------------------------------------- /android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'com.google.protobuf' 5 | 6 | android { 7 | compileSdkVersion 29 8 | defaultConfig { 9 | applicationId "fr.fbernard.newsapp.android" 10 | minSdkVersion 21 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | buildConfigField "String", "GRPC_NEWS_HOST", "\"10.0.2.2\"" 16 | buildConfigField "int", "GRPC_NEWS_PORT", "6565" 17 | } 18 | 19 | 20 | sourceSets { 21 | main { 22 | proto { 23 | srcDir '../../protos/' 24 | } 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | } 35 | 36 | 37 | protobuf { 38 | protoc { 39 | artifact = "com.google.protobuf:protoc:$protoc_version" 40 | } 41 | plugins { 42 | javalite { 43 | artifact = "com.google.protobuf:protoc-gen-javalite:$javalite_version" 44 | } 45 | grpc { 46 | artifact = "io.grpc:protoc-gen-grpc-java:$grpc_version" 47 | } 48 | rxgrpc { 49 | artifact = "com.salesforce.servicelibs:rxgrpc:$rxgrpc_version" 50 | } 51 | } 52 | generateProtoTasks { 53 | all().each { task -> 54 | task.plugins { 55 | javalite {} 56 | grpc { 57 | option 'lite' 58 | } 59 | rxgrpc {} 60 | } 61 | } 62 | } 63 | } 64 | 65 | dependencies { 66 | implementation fileTree(dir: 'libs', include: ['*.jar']) 67 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | implementation 'androidx.appcompat:appcompat:1.1.0' 69 | implementation 'com.google.android.material:material:1.0.0' 70 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 71 | implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' 72 | 73 | //GRPC 74 | implementation "io.grpc:grpc-okhttp:$grpc_version" 75 | implementation "io.grpc:grpc-protobuf-lite:$grpc_version" 76 | implementation "io.grpc:grpc-stub:$grpc_version" 77 | implementation 'javax.annotation:javax.annotation-api:1.3.2' 78 | implementation "com.salesforce.servicelibs:rxgrpc-stub:$rxgrpc_version" 79 | implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version" 80 | implementation "io.reactivex.rxjava2:rxjava:$rxjava_version" 81 | 82 | 83 | //GLIDE 84 | implementation "com.github.bumptech.glide:glide:$glide_version" 85 | 86 | //TESTS 87 | testImplementation 'junit:junit:4.12' 88 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 89 | androidTestImplementation 'androidx.test:runner:1.2.0' 90 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 91 | } 92 | -------------------------------------------------------------------------------- /android/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 22 | -------------------------------------------------------------------------------- /android/app/src/androidTest/java/fr/fbernard/newsapp/android/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.android 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().context 22 | assertEquals("fr.fbernard.newsapp.android", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/app/src/main/java/fr/fbernard/newsapp/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.android 2 | import androidx.lifecycle.Observer 3 | import androidx.lifecycle.ViewModelProviders 4 | import android.os.Bundle 5 | import com.google.android.material.bottomnavigation.BottomNavigationView 6 | import com.google.android.material.snackbar.Snackbar 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import fr.fbernard.grpc.news.Topic 10 | import kotlinx.android.synthetic.main.activity_main.* 11 | 12 | class MainActivity : AppCompatActivity() { 13 | 14 | 15 | 16 | private val mainViewModel : MainViewModel by lazy { 17 | ViewModelProviders.of(this).get(MainViewModel::class.java) 18 | } 19 | 20 | private val newsAdapter = NewsAdapter() 21 | private lateinit var layoutManager : LinearLayoutManager 22 | 23 | private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> 24 | when (item.itemId) { 25 | R.id.navigation_sport -> { 26 | mainTitle.setText(R.string.title_sport) 27 | updateTopic(Topic.SPORT) 28 | true 29 | } 30 | R.id.navigation_tech -> { 31 | mainTitle.setText(R.string.title_tech) 32 | updateTopic(Topic.TECH) 33 | true 34 | } 35 | R.id.navigation_economy -> { 36 | mainTitle.setText(R.string.title_economy) 37 | updateTopic(Topic.ECONOMY) 38 | true 39 | } 40 | else->false 41 | } 42 | } 43 | 44 | override fun onCreate(savedInstanceState: Bundle?) { 45 | super.onCreate(savedInstanceState) 46 | setContentView(R.layout.activity_main) 47 | navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener) 48 | 49 | layoutManager = LinearLayoutManager(this) 50 | 51 | newsList.adapter = newsAdapter 52 | newsList.layoutManager = layoutManager 53 | 54 | mainViewModel.news.observe(this, Observer {newsList-> 55 | newsList?.let { 56 | //update adapters item 57 | newsAdapter.updateNews(it) 58 | //reset scroll 59 | layoutManager.scrollToPosition(0) 60 | } 61 | }) 62 | 63 | mainViewModel.addNewsEvent.observe(this, Observer {news-> 64 | news?.let { 65 | newsAdapter.addNews(it) 66 | showSnackBar() 67 | } 68 | }) 69 | 70 | updateTopic(Topic.TECH) 71 | 72 | } 73 | 74 | 75 | private fun updateTopic(topic: Topic){ 76 | mainViewModel.getNews(topic) 77 | mainViewModel.subscribe(topic) 78 | } 79 | 80 | private fun showSnackBar(){ 81 | Snackbar.make(newsList,"Breaking News !", Snackbar.LENGTH_LONG) 82 | .setAction("show") { 83 | layoutManager.scrollToPosition(newsAdapter.itemCount - 1) 84 | }.show() 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /android/app/src/main/java/fr/fbernard/newsapp/android/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.android 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import fr.fbernard.grpc.news.News 6 | import fr.fbernard.grpc.news.Topic 7 | import fr.fbernard.newsapp.android.data.NewsDataService 8 | import io.reactivex.android.schedulers.AndroidSchedulers 9 | import io.reactivex.disposables.CompositeDisposable 10 | import io.reactivex.disposables.Disposable 11 | import io.reactivex.schedulers.Schedulers 12 | 13 | class MainViewModel : ViewModel() { 14 | 15 | val news = MutableLiveData>() 16 | val addNewsEvent = MutableLiveData() 17 | val error = MutableLiveData() 18 | private val disposableList = CompositeDisposable() 19 | private var subscriptionDisposable : Disposable? = null 20 | 21 | 22 | fun getNews(topic: Topic){ 23 | val disposable = NewsDataService.getNews(topic) 24 | .subscribeOn(Schedulers.io()) 25 | .observeOn(AndroidSchedulers.mainThread()) 26 | .subscribe({ 27 | news.postValue(it.newsList) 28 | },{ 29 | error.postValue(it.localizedMessage) 30 | }) 31 | 32 | disposableList.add(disposable) 33 | } 34 | 35 | 36 | fun subscribe(topic: Topic){ 37 | subscriptionDisposable?.dispose() 38 | subscriptionDisposable = NewsDataService.subscribe(topic) 39 | .subscribeOn(Schedulers.io()) 40 | .observeOn(AndroidSchedulers.mainThread()) 41 | .subscribe({ 42 | addNewsEvent.postValue(it) 43 | },{ 44 | error.postValue(it.localizedMessage) 45 | }) 46 | } 47 | 48 | 49 | override fun onCleared() { 50 | super.onCleared() 51 | disposableList.dispose() 52 | subscriptionDisposable?.dispose() 53 | } 54 | } -------------------------------------------------------------------------------- /android/app/src/main/java/fr/fbernard/newsapp/android/NewsAdapter.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.android 2 | 3 | import android.net.Uri 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.bumptech.glide.Glide 9 | import fr.fbernard.grpc.news.News 10 | import kotlinx.android.synthetic.main.item_news.view.* 11 | 12 | 13 | class NewsAdapter : RecyclerView.Adapter() { 14 | 15 | private val newsList = mutableListOf() 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, position: Int): NewsViewHolder { 18 | val view = LayoutInflater.from(parent.context).inflate(R.layout.item_news,parent,false) 19 | return NewsViewHolder(view) 20 | } 21 | 22 | override fun getItemCount() = newsList.size 23 | 24 | override fun onBindViewHolder(viewHolder: NewsViewHolder, position: Int) { 25 | viewHolder.bind(newsList[position]) 26 | } 27 | 28 | fun updateNews(news:List){ 29 | newsList.clear() 30 | newsList.addAll(news) 31 | notifyDataSetChanged() 32 | } 33 | 34 | 35 | fun addNews(news:News){ 36 | newsList.add(news) 37 | notifyDataSetChanged() 38 | } 39 | 40 | 41 | inner class NewsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){ 42 | fun bind(news:News){ 43 | itemView.newsTitle.text = news.title 44 | itemView.newsDescription.text = news.description 45 | Glide.with(itemView).load(news.imageUrl).into(itemView.newsImage) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /android/app/src/main/java/fr/fbernard/newsapp/android/data/NewsDataService.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.android.data 2 | 3 | import fr.fbernard.grpc.news.* 4 | import fr.fbernard.newsapp.android.BuildConfig 5 | import io.grpc.ManagedChannelBuilder 6 | import io.reactivex.Flowable 7 | import io.reactivex.Single 8 | 9 | object NewsDataService { 10 | 11 | private val newsGrpcService = RxNewsServiceGrpc.newRxStub( 12 | ManagedChannelBuilder.forAddress(BuildConfig.GRPC_NEWS_HOST,BuildConfig.GRPC_NEWS_PORT) 13 | .usePlaintext() 14 | .build()) 15 | 16 | fun getNews(topic: Topic): Single { 17 | return newsGrpcService.getNews(NewsRequest.newBuilder().setTopic(topic).build()) 18 | } 19 | 20 | fun subscribe(topic: Topic): Flowable { 21 | return newsGrpcService.subscribe(SubscribeRequest.newBuilder().setTopic(topic).build()) 22 | } 23 | 24 | fun postNews(news: News):Single{ 25 | return newsGrpcService.postNews(news) 26 | } 27 | 28 | 29 | 30 | 31 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /android/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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_money.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_sport.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_tech.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 23 | 24 | 25 | 32 | 33 | 44 | 45 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/item_news.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 14 | 15 | 22 | 23 | 35 | 36 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /android/app/src/main/res/menu/navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GrpcNewsApp 3 | Sport news 4 | Tech news 5 | Economy news 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/test/java/fr/fbernard/newsapp/android/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.android 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.50' 5 | ext.lifecycle_version = '1.1.1' 6 | ext.grpc_version = '1.23.0' 7 | ext.protoc_version = '3.9.1' 8 | ext.javalite_version = '3.0.0' 9 | ext.rxjava_version = '2.2.12' 10 | ext.rxandroid_version = '2.1.1' 11 | ext.rxgrpc_version = '1.0.0' 12 | ext.glide_version = '4.10.0' 13 | 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | dependencies { 19 | classpath 'com.android.tools.build:gradle:3.5.1' 20 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 21 | classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.10" 22 | } 23 | } 24 | 25 | allprojects { 26 | repositories { 27 | google() 28 | jcenter() 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | 17 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Sep 29 14:21:25 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /backends/kotlin-graal/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | /out/ 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ -------------------------------------------------------------------------------- /backends/kotlin-graal/README.md: -------------------------------------------------------------------------------- 1 | # gRPC Server example 2 | 3 | ## Build & Run Project 4 | 5 | Project require Java 8 (or later) JVM installed. 6 | To build and start server use gradle wrapper command : 7 | 8 | Linux/MacOS 9 | ```SH 10 | ./gradlew run 11 | ``` 12 | 13 | Windows 14 | ```SH 15 | ./gradlew.bat run 16 | ``` 17 | 18 | By default server run on port 6565 19 | 20 | 21 | ## Build & Run native binary with GraalVM 22 | [GraalVM](https://www.graalvm.org/) >= 19.x is required to build native binary 23 | 24 | Use gradle nativeImage custom task: 25 | ``` 26 | ./gradlew nativeImage 27 | ``` 28 | 29 | Use native-image command from project folder: 30 | ``` 31 | native-image -jar build/libs/kotlin-graal-1.0-SNAPSHOT.jar -H:ReflectionConfigurationFiles=./graal-reflection.json --delay-class-initialization-to-runtime=io.netty.handler.codec.http2.Http2CodecUtil,io.netty.handler.codec.http2.DefaultHttp2FrameWriter -H:+ReportExceptionStackTraces --allow-incomplete-classpath 32 | ``` 33 | 34 | Run binary: 35 | ``` 36 | ./kotlin-graal-1.0-SNAPSHOT 37 | ``` -------------------------------------------------------------------------------- /backends/kotlin-graal/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.9.22' 4 | grpcVersion = '1.60.0' 5 | grpcKotlinVersion = '1.4.1' 6 | coroutinesVersion = '1.7.3' 7 | } 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 14 | } 15 | } 16 | 17 | 18 | plugins { 19 | id "com.google.protobuf" version "0.9.4" 20 | id 'idea' 21 | id 'application' 22 | id "org.jetbrains.kotlin.jvm" version "1.9.22" 23 | } 24 | 25 | group 'fr.fbernard.newsapp' 26 | version '1.0-SNAPSHOT' 27 | 28 | repositories { 29 | mavenCentral() 30 | } 31 | 32 | 33 | compileKotlin { 34 | kotlinOptions { 35 | freeCompilerArgs = ["-Xjsr305=strict"] 36 | jvmTarget = "17" 37 | } 38 | } 39 | compileTestKotlin { 40 | kotlinOptions { 41 | freeCompilerArgs = ["-Xjsr305=strict"] 42 | jvmTarget = "17" 43 | } 44 | } 45 | 46 | 47 | sourceSets { 48 | generated{ 49 | java.srcDir "${buildDir}/generated/src/proto/" 50 | } 51 | main { 52 | proto { 53 | srcDir '../../protos/' 54 | } 55 | } 56 | } 57 | 58 | 59 | jar { 60 | manifest { 61 | attributes 'Main-Class': 'fr.fbernard.newsapp.backend.NewsAppServer' 62 | } 63 | from { 64 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 65 | } 66 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 67 | } 68 | 69 | dependencies{ 70 | implementation("org.jetbrains.kotlin:kotlin-stdlib") 71 | implementation("io.grpc:grpc-netty:$grpcVersion") 72 | implementation("io.grpc:grpc-protobuf:$grpcVersion") 73 | implementation("io.grpc:grpc-stub:$grpcVersion") 74 | implementation("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion") 75 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") 76 | implementation("javax.annotation:javax.annotation-api:1.2") 77 | } 78 | 79 | mainClassName = "fr.fbernard.newsapp.backend.NewsAppServer" 80 | 81 | protobuf { 82 | protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } 83 | plugins { 84 | grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } 85 | grpckt { artifact = "io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion:jdk8@jar" } 86 | } 87 | generateProtoTasks { 88 | all()*.plugins { 89 | grpc {} 90 | grpckt{} 91 | } 92 | } 93 | } 94 | 95 | task nativeImage(dependsOn: jar, type: Exec) { 96 | commandLine 'native-image' ,"-jar", "${jar.archiveFile.get().getAsFile()}", "-H:ReflectionConfigurationFiles=$project.rootDir/graal-reflection.json" ,"--initialize-at-run-time=io.netty.handler.codec.http2.Http2CodecUtil,io.netty.handler.codec.http2.DefaultHttp2FrameWriter", "-H:+ReportExceptionStackTraces", "--allow-incomplete-classpath" 97 | } -------------------------------------------------------------------------------- /backends/kotlin-graal/graal-reflection.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "io.netty.channel.socket.nio.NioServerSocketChannel", 4 | "methods": [ 5 | { "name": "", "parameterTypes": [] } 6 | ] 7 | } 8 | ] -------------------------------------------------------------------------------- /backends/kotlin-graal/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.jvmargs=-Xmx2048m -------------------------------------------------------------------------------- /backends/kotlin-graal/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/backends/kotlin-graal/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /backends/kotlin-graal/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /backends/kotlin-graal/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /backends/kotlin-graal/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /backends/kotlin-graal/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kotlin-graal' 2 | 3 | -------------------------------------------------------------------------------- /backends/kotlin-graal/src/main/kotlin/fr/fbernard/newsapp/backend/NewsAppServer.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.backend 2 | 3 | import io.grpc.Server 4 | import io.grpc.ServerBuilder 5 | import sun.misc.Signal 6 | import java.io.IOException 7 | 8 | class NewsAppServer { 9 | 10 | companion object { 11 | @Throws(IOException::class, InterruptedException::class) 12 | @JvmStatic 13 | fun main(args: Array) { 14 | val server = NewsAppServer() 15 | args.firstOrNull()?.toIntOrNull()?.let { 16 | server.start(it) 17 | } ?: server.start() 18 | server.blockUntilShutdown() 19 | } 20 | } 21 | 22 | private var server:Server?=null 23 | 24 | @Throws(IOException::class) 25 | private fun start(port:Int=6565) { 26 | Signal.handle(Signal("INT")) { System.exit(0) } 27 | server = ServerBuilder.forPort(port) 28 | .addService(NewsService()) 29 | .build() 30 | .start() 31 | println("News GRPC server started, listening on $port") 32 | Runtime.getRuntime().addShutdownHook(object : Thread() { 33 | override fun run() { 34 | this@NewsAppServer.stop() 35 | println("News GRPC server shut down") 36 | } 37 | }) 38 | } 39 | 40 | private fun stop() { 41 | server?.shutdown() 42 | } 43 | 44 | 45 | @Throws(InterruptedException::class) 46 | private fun blockUntilShutdown() { 47 | server?.awaitTermination() 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /backends/kotlin-graal/src/main/kotlin/fr/fbernard/newsapp/backend/NewsService.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.backend 2 | 3 | import fr.fbernard.grpc.news.News 4 | import fr.fbernard.grpc.news.NewsRequest 5 | import fr.fbernard.grpc.news.NewsResponse 6 | import fr.fbernard.grpc.news.NewsServiceGrpcKt 7 | import fr.fbernard.grpc.news.SubscribeRequest 8 | import fr.fbernard.newsapp.backend.data.NewsStore 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.MutableSharedFlow 11 | import kotlinx.coroutines.flow.filter 12 | 13 | class NewsService : NewsServiceGrpcKt.NewsServiceCoroutineImplBase() { 14 | 15 | private val addNewEvent = MutableSharedFlow() 16 | 17 | override suspend fun getNews(request: NewsRequest): NewsResponse { 18 | val news = NewsStore.getByTopic(request.topic) 19 | return NewsResponse.newBuilder().addAllNews(news).build() 20 | } 21 | 22 | //@FlowPreview 23 | override fun subscribe(request: SubscribeRequest): Flow { 24 | return addNewEvent.filter { it.topic == request.topic } 25 | } 26 | 27 | override suspend fun postNews(request: News): News { 28 | return NewsStore.saveNews(request)?.let {saved-> 29 | addNewEvent.emit(saved) 30 | saved 31 | } ?: throw IllegalStateException("Save news failed") 32 | } 33 | } -------------------------------------------------------------------------------- /backends/kotlin-graal/src/main/kotlin/fr/fbernard/newsapp/backend/data/NewsStore.kt: -------------------------------------------------------------------------------- 1 | package fr.fbernard.newsapp.backend.data 2 | 3 | import fr.fbernard.grpc.news.News 4 | import fr.fbernard.grpc.news.Topic 5 | import java.util.concurrent.ConcurrentHashMap 6 | 7 | object NewsStore { 8 | private val data = ConcurrentHashMap>() 9 | 10 | //Populate 10 news by topic 11 | init { 12 | Topic.entries.filter { it !=Topic.UNRECOGNIZED }.forEach { 13 | val newsList = mutableListOf() 14 | for( i in 0..9){ 15 | val news = News.newBuilder().setTopic(it) 16 | .setNewsId("${it.name}-$i") 17 | .setTitle("${it.name} Title $i") 18 | .setDescription("${it.name.lowercase()} description $i") 19 | .setImageUrl("https://picsum.photos/400/400/?${it.name}-$i").build() 20 | newsList.add(news) 21 | } 22 | data[it] = newsList 23 | } 24 | } 25 | 26 | 27 | fun saveNews(news: News):News?{ 28 | return data[news.topic]?.let { 29 | val id = "${news.topic.name}-${it.size+1}" 30 | val toAdd = news.toBuilder().setNewsId(id).build() 31 | it.add(toAdd) 32 | toAdd 33 | } 34 | } 35 | 36 | fun getByTopic(topic: Topic):List{ 37 | return data[topic]?.toList() ?: emptyList() 38 | } 39 | } -------------------------------------------------------------------------------- /docs/android_news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/docs/android_news.png -------------------------------------------------------------------------------- /docs/android_news_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/docs/android_news_added.png -------------------------------------------------------------------------------- /docs/ios_news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/docs/ios_news.png -------------------------------------------------------------------------------- /docs/ios_news_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/docs/ios_news_added.png -------------------------------------------------------------------------------- /docs/web_news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/docs/web_news.png -------------------------------------------------------------------------------- /docs/web_news_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/docs/web_news_added.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://www.gitignore.io 2 | *.generated.swift 3 | ### Xcode ### 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.xcuserstate 18 | Pods/** 19 | # Pod 20 | ./Pods 21 | Pods 22 | Pods/* 23 | Pods/ 24 | ios.xcworkspace 25 | ios.xcodeproj/xcuserdata/* 26 | ios.xcodeproj/project.xcworkspace/xcuserdata/* 27 | # General 28 | *.swp 29 | *.swo 30 | *.rbo 31 | *.gem 32 | .DS_Store 33 | .rbenv-version 34 | .rbx/ 35 | xcuserdata 36 | DerivedData 37 | /concatenated.* 38 | *.xcuserstate -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'ios' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for ios 9 | pod 'SwiftGRPC' 10 | pod 'MaterialComponents/Snackbar' 11 | end 12 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - BoringSSL-GRPC (0.0.3): 3 | - BoringSSL-GRPC/Implementation (= 0.0.3) 4 | - BoringSSL-GRPC/Interface (= 0.0.3) 5 | - BoringSSL-GRPC/Implementation (0.0.3): 6 | - BoringSSL-GRPC/Interface (= 0.0.3) 7 | - BoringSSL-GRPC/Interface (0.0.3) 8 | - gRPC-Core (1.19.0): 9 | - gRPC-Core/Implementation (= 1.19.0) 10 | - gRPC-Core/Interface (= 1.19.0) 11 | - gRPC-Core/Implementation (1.19.0): 12 | - BoringSSL-GRPC (= 0.0.3) 13 | - gRPC-Core/Interface (= 1.19.0) 14 | - nanopb (~> 0.3) 15 | - gRPC-Core/Interface (1.19.0) 16 | - MaterialComponents/AnimationTiming (89.0.0) 17 | - MaterialComponents/Buttons (89.0.0): 18 | - MaterialComponents/Elevation 19 | - MaterialComponents/Ink 20 | - MaterialComponents/private/Math 21 | - MaterialComponents/Ripple 22 | - MaterialComponents/ShadowElevations 23 | - MaterialComponents/ShadowLayer 24 | - MaterialComponents/Shapes 25 | - MaterialComponents/Typography 26 | - MDFInternationalization 27 | - MDFTextAccessibility 28 | - MaterialComponents/Elevation (89.0.0): 29 | - MaterialComponents/private/Color 30 | - MaterialComponents/private/Math 31 | - MaterialComponents/Ink (89.0.0): 32 | - MaterialComponents/private/Color 33 | - MaterialComponents/private/Math 34 | - MaterialComponents/OverlayWindow (89.0.0): 35 | - MaterialComponents/private/Application 36 | - MaterialComponents/private/Application (89.0.0) 37 | - MaterialComponents/private/Color (89.0.0) 38 | - MaterialComponents/private/KeyboardWatcher (89.0.0): 39 | - MaterialComponents/private/Application 40 | - MaterialComponents/private/Math (89.0.0) 41 | - MaterialComponents/private/Overlay (89.0.0) 42 | - MaterialComponents/Ripple (89.0.0): 43 | - MaterialComponents/AnimationTiming 44 | - MaterialComponents/private/Color 45 | - MaterialComponents/private/Math 46 | - MaterialComponents/ShadowElevations (89.0.0) 47 | - MaterialComponents/ShadowLayer (89.0.0): 48 | - MaterialComponents/ShadowElevations 49 | - MaterialComponents/Shapes (89.0.0): 50 | - MaterialComponents/private/Color 51 | - MaterialComponents/private/Math 52 | - MaterialComponents/ShadowLayer 53 | - MaterialComponents/Snackbar (89.0.0): 54 | - MaterialComponents/AnimationTiming 55 | - MaterialComponents/Buttons 56 | - MaterialComponents/Elevation 57 | - MaterialComponents/OverlayWindow 58 | - MaterialComponents/private/Application 59 | - MaterialComponents/private/KeyboardWatcher 60 | - MaterialComponents/private/Math 61 | - MaterialComponents/private/Overlay 62 | - MaterialComponents/ShadowElevations 63 | - MaterialComponents/ShadowLayer 64 | - MaterialComponents/Typography 65 | - MaterialComponents/Typography (89.0.0): 66 | - MaterialComponents/private/Application 67 | - MaterialComponents/private/Math 68 | - MDFInternationalization (2.0.0) 69 | - MDFTextAccessibility (2.0.0) 70 | - nanopb (0.3.901): 71 | - nanopb/decode (= 0.3.901) 72 | - nanopb/encode (= 0.3.901) 73 | - nanopb/decode (0.3.901) 74 | - nanopb/encode (0.3.901) 75 | - SwiftGRPC (0.9.1): 76 | - gRPC-Core (~> 1.19.0) 77 | - SwiftProtobuf (~> 1.5.0) 78 | - SwiftProtobuf (1.5.0) 79 | 80 | DEPENDENCIES: 81 | - MaterialComponents/Snackbar 82 | - SwiftGRPC 83 | 84 | SPEC REPOS: 85 | https://github.com/cocoapods/specs.git: 86 | - BoringSSL-GRPC 87 | - gRPC-Core 88 | - MaterialComponents 89 | - MDFInternationalization 90 | - MDFTextAccessibility 91 | - nanopb 92 | - SwiftGRPC 93 | - SwiftProtobuf 94 | 95 | SPEC CHECKSUMS: 96 | BoringSSL-GRPC: db8764df3204ccea016e1c8dd15d9a9ad63ff318 97 | gRPC-Core: bd9472c8daa2e414b9f8038ba667bf56ce0e02b8 98 | MaterialComponents: 82a817165f3ca2df0e3f321a9d05d08bb152c7f8 99 | MDFInternationalization: 010097556d6b09d2c4ea38e0820ea6d37be6a314 100 | MDFTextAccessibility: 85c09a1bd9c321f494348e632a25063bcda35a53 101 | nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48 102 | SwiftGRPC: 43d3e8db88f97e7386bd44f159ecdfad02ce3362 103 | SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2 104 | 105 | PODFILE CHECKSUM: 453cd5bb2afd7b13a3511abce7c1abd8831f4715 106 | 107 | COCOAPODS: 1.7.1 108 | -------------------------------------------------------------------------------- /ios/README.md: -------------------------------------------------------------------------------- 1 | # gRPC News iOS client 2 | 3 | ## Overview 4 | 5 | 6 | 7 | ## Build & Run Project 8 | 9 | Use Xcode 10 and cocoapod to build application: 10 | 11 | ```SH 12 | pod install 13 | open ios.xcworkspace 14 | ``` 15 | 16 | ## Libraries used 17 | 18 | * [gRPC Swift](https://github.com/grpc/grpc-swift) 19 | * [MaterialComponents (SnackBar)](https://material.io/develop/ios/components/snackbars/) 20 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 319F8F4F215EB2620074277A /* NewsDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319F8F4E215EB2620074277A /* NewsDataService.swift */; }; 11 | 319F8F51215EE12D0074277A /* NewsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319F8F50215EE12D0074277A /* NewsTableViewCell.swift */; }; 12 | 31D40109215D68ED0027214C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D40108215D68ED0027214C /* AppDelegate.swift */; }; 13 | 31D4010B215D68ED0027214C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D4010A215D68ED0027214C /* ViewController.swift */; }; 14 | 31D4010E215D68ED0027214C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 31D4010C215D68ED0027214C /* Main.storyboard */; }; 15 | 31D40110215D68EE0027214C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31D4010F215D68EE0027214C /* Assets.xcassets */; }; 16 | 31D40113215D68EE0027214C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 31D40111215D68EE0027214C /* LaunchScreen.storyboard */; }; 17 | 31F1AD3F215D7AA4000E0165 /* news_service.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F1AD3D215D7AA4000E0165 /* news_service.grpc.swift */; }; 18 | 31F1AD40215D7AA4000E0165 /* news_service.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F1AD3E215D7AA4000E0165 /* news_service.pb.swift */; }; 19 | E811BF29D4026E41F5EB6822 /* Pods_ios.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C18CE1A54FC5927F0EAA3F4A /* Pods_ios.framework */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 319F8F4E215EB2620074277A /* NewsDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsDataService.swift; sourceTree = ""; }; 24 | 319F8F50215EE12D0074277A /* NewsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsTableViewCell.swift; sourceTree = ""; }; 25 | 31D40105215D68ED0027214C /* ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ios.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 31D40108215D68ED0027214C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 27 | 31D4010A215D68ED0027214C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 28 | 31D4010D215D68ED0027214C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 31D4010F215D68EE0027214C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 31D40112215D68EE0027214C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 31D40114215D68EE0027214C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 31F1AD3D215D7AA4000E0165 /* news_service.grpc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = news_service.grpc.swift; sourceTree = ""; }; 33 | 31F1AD3E215D7AA4000E0165 /* news_service.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = news_service.pb.swift; sourceTree = ""; }; 34 | 3308D74D8FE1CA12077504A7 /* Pods-ios.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios.release.xcconfig"; path = "Pods/Target Support Files/Pods-ios/Pods-ios.release.xcconfig"; sourceTree = ""; }; 35 | 3D036E329AAD6DA5861CE0BF /* Pods-ios.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ios/Pods-ios.debug.xcconfig"; sourceTree = ""; }; 36 | C18CE1A54FC5927F0EAA3F4A /* Pods_ios.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ios.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 31D40102215D68ED0027214C /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | E811BF29D4026E41F5EB6822 /* Pods_ios.framework in Frameworks */, 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 31D400FC215D68ED0027214C = { 52 | isa = PBXGroup; 53 | children = ( 54 | 31D40107215D68ED0027214C /* ios */, 55 | 31D40106215D68ED0027214C /* Products */, 56 | 642F177D805CCDAB9F6827ED /* Pods */, 57 | 5E17E28F48E28CA9A335F4B2 /* Frameworks */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | 31D40106215D68ED0027214C /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 31D40105215D68ED0027214C /* ios.app */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | 31D40107215D68ED0027214C /* ios */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 31F1AD3D215D7AA4000E0165 /* news_service.grpc.swift */, 73 | 31F1AD3E215D7AA4000E0165 /* news_service.pb.swift */, 74 | 31D40108215D68ED0027214C /* AppDelegate.swift */, 75 | 31D4010A215D68ED0027214C /* ViewController.swift */, 76 | 31D4010C215D68ED0027214C /* Main.storyboard */, 77 | 31D4010F215D68EE0027214C /* Assets.xcassets */, 78 | 31D40111215D68EE0027214C /* LaunchScreen.storyboard */, 79 | 31D40114215D68EE0027214C /* Info.plist */, 80 | 319F8F4E215EB2620074277A /* NewsDataService.swift */, 81 | 319F8F50215EE12D0074277A /* NewsTableViewCell.swift */, 82 | ); 83 | path = ios; 84 | sourceTree = ""; 85 | }; 86 | 5E17E28F48E28CA9A335F4B2 /* Frameworks */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | C18CE1A54FC5927F0EAA3F4A /* Pods_ios.framework */, 90 | ); 91 | name = Frameworks; 92 | sourceTree = ""; 93 | }; 94 | 642F177D805CCDAB9F6827ED /* Pods */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 3D036E329AAD6DA5861CE0BF /* Pods-ios.debug.xcconfig */, 98 | 3308D74D8FE1CA12077504A7 /* Pods-ios.release.xcconfig */, 99 | ); 100 | name = Pods; 101 | sourceTree = ""; 102 | }; 103 | /* End PBXGroup section */ 104 | 105 | /* Begin PBXNativeTarget section */ 106 | 31D40104215D68ED0027214C /* ios */ = { 107 | isa = PBXNativeTarget; 108 | buildConfigurationList = 31D40117215D68EE0027214C /* Build configuration list for PBXNativeTarget "ios" */; 109 | buildPhases = ( 110 | 503FEA4C173F9B8BA3871949 /* [CP] Check Pods Manifest.lock */, 111 | 31D40101215D68ED0027214C /* Sources */, 112 | 31D40102215D68ED0027214C /* Frameworks */, 113 | 31D40103215D68ED0027214C /* Resources */, 114 | F496B8D3D7602AD952C78212 /* [CP] Embed Pods Frameworks */, 115 | ); 116 | buildRules = ( 117 | ); 118 | dependencies = ( 119 | ); 120 | name = ios; 121 | productName = ios; 122 | productReference = 31D40105215D68ED0027214C /* ios.app */; 123 | productType = "com.apple.product-type.application"; 124 | }; 125 | /* End PBXNativeTarget section */ 126 | 127 | /* Begin PBXProject section */ 128 | 31D400FD215D68ED0027214C /* Project object */ = { 129 | isa = PBXProject; 130 | attributes = { 131 | LastSwiftUpdateCheck = 1000; 132 | LastUpgradeCheck = 1000; 133 | ORGANIZATIONNAME = "florian bernard"; 134 | TargetAttributes = { 135 | 31D40104215D68ED0027214C = { 136 | CreatedOnToolsVersion = 10.0; 137 | }; 138 | }; 139 | }; 140 | buildConfigurationList = 31D40100215D68ED0027214C /* Build configuration list for PBXProject "ios" */; 141 | compatibilityVersion = "Xcode 9.3"; 142 | developmentRegion = en; 143 | hasScannedForEncodings = 0; 144 | knownRegions = ( 145 | en, 146 | Base, 147 | ); 148 | mainGroup = 31D400FC215D68ED0027214C; 149 | productRefGroup = 31D40106215D68ED0027214C /* Products */; 150 | projectDirPath = ""; 151 | projectRoot = ""; 152 | targets = ( 153 | 31D40104215D68ED0027214C /* ios */, 154 | ); 155 | }; 156 | /* End PBXProject section */ 157 | 158 | /* Begin PBXResourcesBuildPhase section */ 159 | 31D40103215D68ED0027214C /* Resources */ = { 160 | isa = PBXResourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 31D40113215D68EE0027214C /* LaunchScreen.storyboard in Resources */, 164 | 31D40110215D68EE0027214C /* Assets.xcassets in Resources */, 165 | 31D4010E215D68ED0027214C /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 503FEA4C173F9B8BA3871949 /* [CP] Check Pods Manifest.lock */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputFileListPaths = ( 178 | ); 179 | inputPaths = ( 180 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 181 | "${PODS_ROOT}/Manifest.lock", 182 | ); 183 | name = "[CP] Check Pods Manifest.lock"; 184 | outputFileListPaths = ( 185 | ); 186 | outputPaths = ( 187 | "$(DERIVED_FILE_DIR)/Pods-ios-checkManifestLockResult.txt", 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | shellPath = /bin/sh; 191 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 192 | showEnvVarsInLog = 0; 193 | }; 194 | F496B8D3D7602AD952C78212 /* [CP] Embed Pods Frameworks */ = { 195 | isa = PBXShellScriptBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | ); 199 | inputFileListPaths = ( 200 | "${PODS_ROOT}/Target Support Files/Pods-ios/Pods-ios-frameworks-${CONFIGURATION}-input-files.xcfilelist", 201 | ); 202 | name = "[CP] Embed Pods Frameworks"; 203 | outputFileListPaths = ( 204 | "${PODS_ROOT}/Target Support Files/Pods-ios/Pods-ios-frameworks-${CONFIGURATION}-output-files.xcfilelist", 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | shellPath = /bin/sh; 208 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ios/Pods-ios-frameworks.sh\"\n"; 209 | showEnvVarsInLog = 0; 210 | }; 211 | /* End PBXShellScriptBuildPhase section */ 212 | 213 | /* Begin PBXSourcesBuildPhase section */ 214 | 31D40101215D68ED0027214C /* Sources */ = { 215 | isa = PBXSourcesBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | 319F8F4F215EB2620074277A /* NewsDataService.swift in Sources */, 219 | 31F1AD3F215D7AA4000E0165 /* news_service.grpc.swift in Sources */, 220 | 319F8F51215EE12D0074277A /* NewsTableViewCell.swift in Sources */, 221 | 31F1AD40215D7AA4000E0165 /* news_service.pb.swift in Sources */, 222 | 31D4010B215D68ED0027214C /* ViewController.swift in Sources */, 223 | 31D40109215D68ED0027214C /* AppDelegate.swift in Sources */, 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | /* End PBXSourcesBuildPhase section */ 228 | 229 | /* Begin PBXVariantGroup section */ 230 | 31D4010C215D68ED0027214C /* Main.storyboard */ = { 231 | isa = PBXVariantGroup; 232 | children = ( 233 | 31D4010D215D68ED0027214C /* Base */, 234 | ); 235 | name = Main.storyboard; 236 | sourceTree = ""; 237 | }; 238 | 31D40111215D68EE0027214C /* LaunchScreen.storyboard */ = { 239 | isa = PBXVariantGroup; 240 | children = ( 241 | 31D40112215D68EE0027214C /* Base */, 242 | ); 243 | name = LaunchScreen.storyboard; 244 | sourceTree = ""; 245 | }; 246 | /* End PBXVariantGroup section */ 247 | 248 | /* Begin XCBuildConfiguration section */ 249 | 31D40115215D68EE0027214C /* Debug */ = { 250 | isa = XCBuildConfiguration; 251 | buildSettings = { 252 | ALWAYS_SEARCH_USER_PATHS = NO; 253 | CLANG_ANALYZER_NONNULL = YES; 254 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 255 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 256 | CLANG_CXX_LIBRARY = "libc++"; 257 | CLANG_ENABLE_MODULES = YES; 258 | CLANG_ENABLE_OBJC_ARC = YES; 259 | CLANG_ENABLE_OBJC_WEAK = YES; 260 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 261 | CLANG_WARN_BOOL_CONVERSION = YES; 262 | CLANG_WARN_COMMA = YES; 263 | CLANG_WARN_CONSTANT_CONVERSION = YES; 264 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 265 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 266 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 267 | CLANG_WARN_EMPTY_BODY = YES; 268 | CLANG_WARN_ENUM_CONVERSION = YES; 269 | CLANG_WARN_INFINITE_RECURSION = YES; 270 | CLANG_WARN_INT_CONVERSION = YES; 271 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 272 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 273 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 274 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 275 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 276 | CLANG_WARN_STRICT_PROTOTYPES = YES; 277 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 278 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 279 | CLANG_WARN_UNREACHABLE_CODE = YES; 280 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 281 | CODE_SIGN_IDENTITY = "iPhone Developer"; 282 | COPY_PHASE_STRIP = NO; 283 | DEBUG_INFORMATION_FORMAT = dwarf; 284 | ENABLE_STRICT_OBJC_MSGSEND = YES; 285 | ENABLE_TESTABILITY = YES; 286 | GCC_C_LANGUAGE_STANDARD = gnu11; 287 | GCC_DYNAMIC_NO_PIC = NO; 288 | GCC_NO_COMMON_BLOCKS = YES; 289 | GCC_OPTIMIZATION_LEVEL = 0; 290 | GCC_PREPROCESSOR_DEFINITIONS = ( 291 | "DEBUG=1", 292 | "$(inherited)", 293 | ); 294 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 295 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 296 | GCC_WARN_UNDECLARED_SELECTOR = YES; 297 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 298 | GCC_WARN_UNUSED_FUNCTION = YES; 299 | GCC_WARN_UNUSED_VARIABLE = YES; 300 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 301 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 302 | MTL_FAST_MATH = YES; 303 | ONLY_ACTIVE_ARCH = YES; 304 | SDKROOT = iphoneos; 305 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 306 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 307 | }; 308 | name = Debug; 309 | }; 310 | 31D40116215D68EE0027214C /* Release */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ALWAYS_SEARCH_USER_PATHS = NO; 314 | CLANG_ANALYZER_NONNULL = YES; 315 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 316 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 317 | CLANG_CXX_LIBRARY = "libc++"; 318 | CLANG_ENABLE_MODULES = YES; 319 | CLANG_ENABLE_OBJC_ARC = YES; 320 | CLANG_ENABLE_OBJC_WEAK = YES; 321 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 322 | CLANG_WARN_BOOL_CONVERSION = YES; 323 | CLANG_WARN_COMMA = YES; 324 | CLANG_WARN_CONSTANT_CONVERSION = YES; 325 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 326 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 327 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 328 | CLANG_WARN_EMPTY_BODY = YES; 329 | CLANG_WARN_ENUM_CONVERSION = YES; 330 | CLANG_WARN_INFINITE_RECURSION = YES; 331 | CLANG_WARN_INT_CONVERSION = YES; 332 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 333 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 334 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 335 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 336 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 337 | CLANG_WARN_STRICT_PROTOTYPES = YES; 338 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 339 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 340 | CLANG_WARN_UNREACHABLE_CODE = YES; 341 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 342 | CODE_SIGN_IDENTITY = "iPhone Developer"; 343 | COPY_PHASE_STRIP = NO; 344 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 345 | ENABLE_NS_ASSERTIONS = NO; 346 | ENABLE_STRICT_OBJC_MSGSEND = YES; 347 | GCC_C_LANGUAGE_STANDARD = gnu11; 348 | GCC_NO_COMMON_BLOCKS = YES; 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 356 | MTL_ENABLE_DEBUG_INFO = NO; 357 | MTL_FAST_MATH = YES; 358 | SDKROOT = iphoneos; 359 | SWIFT_COMPILATION_MODE = wholemodule; 360 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 361 | VALIDATE_PRODUCT = YES; 362 | }; 363 | name = Release; 364 | }; 365 | 31D40118215D68EE0027214C /* Debug */ = { 366 | isa = XCBuildConfiguration; 367 | baseConfigurationReference = 3D036E329AAD6DA5861CE0BF /* Pods-ios.debug.xcconfig */; 368 | buildSettings = { 369 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 370 | CODE_SIGN_STYLE = Automatic; 371 | DEVELOPMENT_TEAM = T8U4MZ6BLG; 372 | INFOPLIST_FILE = ios/Info.plist; 373 | LD_RUNPATH_SEARCH_PATHS = ( 374 | "$(inherited)", 375 | "@executable_path/Frameworks", 376 | ); 377 | PRODUCT_BUNDLE_IDENTIFIER = fr.fbernard.newsapp.ios; 378 | PRODUCT_NAME = "$(TARGET_NAME)"; 379 | SWIFT_VERSION = 4.2; 380 | TARGETED_DEVICE_FAMILY = "1,2"; 381 | }; 382 | name = Debug; 383 | }; 384 | 31D40119215D68EE0027214C /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | baseConfigurationReference = 3308D74D8FE1CA12077504A7 /* Pods-ios.release.xcconfig */; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | CODE_SIGN_STYLE = Automatic; 390 | DEVELOPMENT_TEAM = T8U4MZ6BLG; 391 | INFOPLIST_FILE = ios/Info.plist; 392 | LD_RUNPATH_SEARCH_PATHS = ( 393 | "$(inherited)", 394 | "@executable_path/Frameworks", 395 | ); 396 | PRODUCT_BUNDLE_IDENTIFIER = fr.fbernard.newsapp.ios; 397 | PRODUCT_NAME = "$(TARGET_NAME)"; 398 | SWIFT_VERSION = 4.2; 399 | TARGETED_DEVICE_FAMILY = "1,2"; 400 | }; 401 | name = Release; 402 | }; 403 | /* End XCBuildConfiguration section */ 404 | 405 | /* Begin XCConfigurationList section */ 406 | 31D40100215D68ED0027214C /* Build configuration list for PBXProject "ios" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | 31D40115215D68EE0027214C /* Debug */, 410 | 31D40116215D68EE0027214C /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | 31D40117215D68EE0027214C /* Build configuration list for PBXNativeTarget "ios" */ = { 416 | isa = XCConfigurationList; 417 | buildConfigurations = ( 418 | 31D40118215D68EE0027214C /* Debug */, 419 | 31D40119215D68EE0027214C /* Release */, 420 | ); 421 | defaultConfigurationIsVisible = 0; 422 | defaultConfigurationName = Release; 423 | }; 424 | /* End XCConfigurationList section */ 425 | }; 426 | rootObject = 31D400FD215D68ED0027214C /* Project object */; 427 | } 428 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/ios/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ios 4 | // 5 | // Created by florian bernard on 27/09/2018. 6 | // Copyright © 2018 florian bernard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/economy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "money.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "money-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/economy.imageset/money-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/ios/ios/Assets.xcassets/economy.imageset/money-1.png -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/economy.imageset/money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/ios/ios/Assets.xcassets/economy.imageset/money.png -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/sport.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sport.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "sport-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/sport.imageset/sport-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/ios/ios/Assets.xcassets/sport.imageset/sport-1.png -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/sport.imageset/sport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/ios/ios/Assets.xcassets/sport.imageset/sport.png -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/tech.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tech.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "tech-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/tech.imageset/tech-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/ios/ios/Assets.xcassets/tech.imageset/tech-1.png -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/tech.imageset/tech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/ios/ios/Assets.xcassets/tech.imageset/tech.png -------------------------------------------------------------------------------- /ios/ios/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ios/ios/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 63 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /ios/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | NewsApp 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/ios/NewsDataService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDataService.swift 3 | // ios 4 | // 5 | // Created by florian bernard on 28/09/2018. 6 | // Copyright © 2018 florian bernard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftGRPC 11 | 12 | final class NewsDataService{ 13 | 14 | static let shared = NewsDataService() 15 | 16 | private let grpcService : Demo_NewsServiceServiceClient 17 | private var addNewsEvent : Demo_NewsServicesubscribeCall? 18 | private var isSubscriptionActive = false 19 | 20 | private init(){ 21 | grpcService = Demo_NewsServiceServiceClient(address:"localhost:6565",secure:false) 22 | } 23 | 24 | 25 | func getNews(topic:Demo_Topic) -> [Demo_News]?{ 26 | do { 27 | var request = Demo_NewsRequest() 28 | request.topic = topic 29 | let response = try grpcService.getNews(request) 30 | return response.news 31 | } catch { 32 | return nil 33 | } 34 | } 35 | 36 | 37 | func subscribe(topic:Demo_Topic,completion: @escaping (Demo_News) -> Void ){ 38 | do{ 39 | cancelSubscription() 40 | var request = Demo_SubscribeRequest() 41 | request.topic = topic 42 | addNewsEvent = try grpcService.subscribe(request) { (CallResult) in 43 | self.isSubscriptionActive = false 44 | } 45 | isSubscriptionActive = true 46 | DispatchQueue.global().async { 47 | do { 48 | while (self.isSubscriptionActive) { 49 | if let news = try self.addNewsEvent?.receive() { 50 | DispatchQueue.main.async { 51 | completion(news) 52 | } 53 | } 54 | } 55 | } 56 | catch { 57 | } 58 | } 59 | } catch{ 60 | } 61 | } 62 | 63 | func cancelSubscription() { 64 | addNewsEvent?.cancel() 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /ios/ios/NewsTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTableViewCell.swift 3 | // ios 4 | // 5 | // Created by florian bernard on 29/09/2018. 6 | // Copyright © 2018 florian bernard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NewsTableViewCell: UITableViewCell { 12 | @IBOutlet weak var titleLabel: UILabel! 13 | @IBOutlet weak var descriptionLabel: UILabel! 14 | @IBOutlet weak var photo: UIImageView! 15 | 16 | override func awakeFromNib() { 17 | super.awakeFromNib() 18 | // Initialization code 19 | } 20 | 21 | override func setSelected(_ selected: Bool, animated: Bool) { 22 | super.setSelected(selected, animated: animated) 23 | 24 | // Configure the view for the selected state 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ios/ios/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ios 4 | // 5 | // Created by florian bernard on 27/09/2018. 6 | // Copyright © 2018 florian bernard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MaterialComponents.MaterialSnackbar 11 | 12 | class ViewController: UIViewController, UITableViewDataSource, UITabBarDelegate { 13 | @IBOutlet weak var tableView: UITableView! 14 | @IBOutlet weak var titleLabel: UILabel! 15 | @IBOutlet weak var techButton: UITabBarItem! 16 | @IBOutlet weak var sportButton: UITabBarItem! 17 | @IBOutlet weak var economyButton: UITabBarItem! 18 | @IBOutlet weak var tabBar: UITabBar! 19 | 20 | var newsList = [Demo_News]() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | // Do any additional setup after loading the view, typically from a nib. 25 | tabBar.selectedItem = techButton 26 | changeTopic(topic: Demo_Topic.tech) 27 | } 28 | 29 | 30 | override func viewWillDisappear(_ animated: Bool) { 31 | NewsDataService.shared.cancelSubscription() 32 | } 33 | 34 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 35 | let theNews = newsList[indexPath.row] 36 | let cell = tableView.dequeueReusableCell(withIdentifier: "newsCell", for: indexPath) as! NewsTableViewCell 37 | 38 | let url = URL(string:theNews.imageURL) 39 | if let imageUrl = url{ 40 | URLSession.shared.dataTask(with: imageUrl) { data, response, error in 41 | guard let data = data, error == nil else { return } 42 | DispatchQueue.main.async() { 43 | cell.photo.image = UIImage(data: data) 44 | } 45 | }.resume() 46 | } 47 | 48 | cell.descriptionLabel.text = theNews.description_p 49 | cell.titleLabel.text = theNews.title 50 | return cell 51 | } 52 | 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | return newsList.count 55 | } 56 | 57 | 58 | func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem){ 59 | 60 | var topic = Demo_Topic.tech 61 | 62 | switch item.tag { 63 | case 0: 64 | topic = Demo_Topic.tech 65 | titleLabel.text = "Tech news" 66 | case 1: 67 | topic = Demo_Topic.sport 68 | titleLabel.text = "Sport news" 69 | case 2: 70 | topic = Demo_Topic.economy 71 | titleLabel.text = "Economy news" 72 | default: 73 | topic = Demo_Topic.tech 74 | titleLabel.text = "Tech news" 75 | } 76 | 77 | changeTopic(topic: topic) 78 | } 79 | 80 | 81 | private func changeTopic(topic:Demo_Topic){ 82 | NewsDataService.shared.subscribe(topic: topic) { addedNews in 83 | self.newsList.append(addedNews) 84 | self.tableView.reloadData() 85 | self.showSnackBar() 86 | } 87 | newsList = NewsDataService.shared.getNews(topic: topic) ?? [Demo_News]() 88 | tableView.reloadData() 89 | } 90 | 91 | 92 | private func showSnackBar(){ 93 | let message = MDCSnackbarMessage() 94 | message.text = "Breaking news" 95 | 96 | let action = MDCSnackbarMessageAction() 97 | let actionHandler = {() in 98 | let indexPath = IndexPath(row: self.newsList.count-1, section: 0) 99 | self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) 100 | } 101 | action.handler = actionHandler 102 | action.title = "show" 103 | message.action = action 104 | MDCSnackbarManager.show(message) 105 | } 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /ios/ios/news_service.grpc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DO NOT EDIT. 3 | // 4 | // Generated by the protocol buffer compiler. 5 | // Source: news_service.proto 6 | // 7 | 8 | // 9 | // Copyright 2018, gRPC Authors All rights reserved. 10 | // 11 | // Licensed under the Apache License, Version 2.0 (the "License"); 12 | // you may not use this file except in compliance with the License. 13 | // You may obtain a copy of the License at 14 | // 15 | // http://www.apache.org/licenses/LICENSE-2.0 16 | // 17 | // Unless required by applicable law or agreed to in writing, software 18 | // distributed under the License is distributed on an "AS IS" BASIS, 19 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | // See the License for the specific language governing permissions and 21 | // limitations under the License. 22 | // 23 | import Foundation 24 | import Dispatch 25 | import SwiftGRPC 26 | import SwiftProtobuf 27 | 28 | internal protocol Demo_NewsServicegetNewsCall: ClientCallUnary {} 29 | 30 | fileprivate final class Demo_NewsServicegetNewsCallBase: ClientCallUnaryBase, Demo_NewsServicegetNewsCall { 31 | override class var method: String { return "/demo.NewsService/getNews" } 32 | } 33 | 34 | internal protocol Demo_NewsServicesubscribeCall: ClientCallServerStreaming { 35 | /// Do not call this directly, call `receive()` in the protocol extension below instead. 36 | func _receive(timeout: DispatchTime) throws -> Demo_News? 37 | /// Call this to wait for a result. Nonblocking. 38 | func receive(completion: @escaping (ResultOrRPCError) -> Void) throws 39 | } 40 | 41 | internal extension Demo_NewsServicesubscribeCall { 42 | /// Call this to wait for a result. Blocking. 43 | func receive(timeout: DispatchTime = .distantFuture) throws -> Demo_News? { return try self._receive(timeout: timeout) } 44 | } 45 | 46 | fileprivate final class Demo_NewsServicesubscribeCallBase: ClientCallServerStreamingBase, Demo_NewsServicesubscribeCall { 47 | override class var method: String { return "/demo.NewsService/subscribe" } 48 | } 49 | 50 | internal protocol Demo_NewsServicepostNewsCall: ClientCallUnary {} 51 | 52 | fileprivate final class Demo_NewsServicepostNewsCallBase: ClientCallUnaryBase, Demo_NewsServicepostNewsCall { 53 | override class var method: String { return "/demo.NewsService/postNews" } 54 | } 55 | 56 | 57 | /// Instantiate Demo_NewsServiceServiceClient, then call methods of this protocol to make API calls. 58 | internal protocol Demo_NewsServiceService: ServiceClient { 59 | /// Synchronous. Unary. 60 | func getNews(_ request: Demo_NewsRequest) throws -> Demo_NewsResponse 61 | /// Asynchronous. Unary. 62 | func getNews(_ request: Demo_NewsRequest, completion: @escaping (Demo_NewsResponse?, CallResult) -> Void) throws -> Demo_NewsServicegetNewsCall 63 | 64 | /// Asynchronous. Server-streaming. 65 | /// Send the initial message. 66 | /// Use methods on the returned object to get streamed responses. 67 | func subscribe(_ request: Demo_SubscribeRequest, completion: ((CallResult) -> Void)?) throws -> Demo_NewsServicesubscribeCall 68 | 69 | /// Synchronous. Unary. 70 | func postNews(_ request: Demo_News) throws -> Demo_News 71 | /// Asynchronous. Unary. 72 | func postNews(_ request: Demo_News, completion: @escaping (Demo_News?, CallResult) -> Void) throws -> Demo_NewsServicepostNewsCall 73 | 74 | } 75 | 76 | internal final class Demo_NewsServiceServiceClient: ServiceClientBase, Demo_NewsServiceService { 77 | /// Synchronous. Unary. 78 | internal func getNews(_ request: Demo_NewsRequest) throws -> Demo_NewsResponse { 79 | return try Demo_NewsServicegetNewsCallBase(channel) 80 | .run(request: request, metadata: metadata) 81 | } 82 | /// Asynchronous. Unary. 83 | internal func getNews(_ request: Demo_NewsRequest, completion: @escaping (Demo_NewsResponse?, CallResult) -> Void) throws -> Demo_NewsServicegetNewsCall { 84 | return try Demo_NewsServicegetNewsCallBase(channel) 85 | .start(request: request, metadata: metadata, completion: completion) 86 | } 87 | 88 | /// Asynchronous. Server-streaming. 89 | /// Send the initial message. 90 | /// Use methods on the returned object to get streamed responses. 91 | internal func subscribe(_ request: Demo_SubscribeRequest, completion: ((CallResult) -> Void)?) throws -> Demo_NewsServicesubscribeCall { 92 | return try Demo_NewsServicesubscribeCallBase(channel) 93 | .start(request: request, metadata: metadata, completion: completion) 94 | } 95 | 96 | /// Synchronous. Unary. 97 | internal func postNews(_ request: Demo_News) throws -> Demo_News { 98 | return try Demo_NewsServicepostNewsCallBase(channel) 99 | .run(request: request, metadata: metadata) 100 | } 101 | /// Asynchronous. Unary. 102 | internal func postNews(_ request: Demo_News, completion: @escaping (Demo_News?, CallResult) -> Void) throws -> Demo_NewsServicepostNewsCall { 103 | return try Demo_NewsServicepostNewsCallBase(channel) 104 | .start(request: request, metadata: metadata, completion: completion) 105 | } 106 | 107 | } 108 | 109 | /// To build a server, implement a class that conforms to this protocol. 110 | /// If one of the methods returning `ServerStatus?` returns nil, 111 | /// it is expected that you have already returned a status to the client by means of `session.close`. 112 | internal protocol Demo_NewsServiceProvider: ServiceProvider { 113 | func getNews(request: Demo_NewsRequest, session: Demo_NewsServicegetNewsSession) throws -> Demo_NewsResponse 114 | func subscribe(request: Demo_SubscribeRequest, session: Demo_NewsServicesubscribeSession) throws -> ServerStatus? 115 | func postNews(request: Demo_News, session: Demo_NewsServicepostNewsSession) throws -> Demo_News 116 | } 117 | 118 | extension Demo_NewsServiceProvider { 119 | internal var serviceName: String { return "demo.NewsService" } 120 | 121 | /// Determines and calls the appropriate request handler, depending on the request's method. 122 | /// Throws `HandleMethodError.unknownMethod` for methods not handled by this service. 123 | internal func handleMethod(_ method: String, handler: Handler) throws -> ServerStatus? { 124 | switch method { 125 | case "/demo.NewsService/getNews": 126 | return try Demo_NewsServicegetNewsSessionBase( 127 | handler: handler, 128 | providerBlock: { try self.getNews(request: $0, session: $1 as! Demo_NewsServicegetNewsSessionBase) }) 129 | .run() 130 | case "/demo.NewsService/subscribe": 131 | return try Demo_NewsServicesubscribeSessionBase( 132 | handler: handler, 133 | providerBlock: { try self.subscribe(request: $0, session: $1 as! Demo_NewsServicesubscribeSessionBase) }) 134 | .run() 135 | case "/demo.NewsService/postNews": 136 | return try Demo_NewsServicepostNewsSessionBase( 137 | handler: handler, 138 | providerBlock: { try self.postNews(request: $0, session: $1 as! Demo_NewsServicepostNewsSessionBase) }) 139 | .run() 140 | default: 141 | throw HandleMethodError.unknownMethod 142 | } 143 | } 144 | } 145 | 146 | internal protocol Demo_NewsServicegetNewsSession: ServerSessionUnary {} 147 | 148 | fileprivate final class Demo_NewsServicegetNewsSessionBase: ServerSessionUnaryBase, Demo_NewsServicegetNewsSession {} 149 | 150 | internal protocol Demo_NewsServicesubscribeSession: ServerSessionServerStreaming { 151 | /// Send a message to the stream. Nonblocking. 152 | func send(_ message: Demo_News, completion: @escaping (Error?) -> Void) throws 153 | /// Do not call this directly, call `send()` in the protocol extension below instead. 154 | func _send(_ message: Demo_News, timeout: DispatchTime) throws 155 | 156 | /// Close the connection and send the status. Non-blocking. 157 | /// This method should be called if and only if your request handler returns a nil value instead of a server status; 158 | /// otherwise SwiftGRPC will take care of sending the status for you. 159 | func close(withStatus status: ServerStatus, completion: (() -> Void)?) throws 160 | } 161 | 162 | internal extension Demo_NewsServicesubscribeSession { 163 | /// Send a message to the stream and wait for the send operation to finish. Blocking. 164 | func send(_ message: Demo_News, timeout: DispatchTime = .distantFuture) throws { try self._send(message, timeout: timeout) } 165 | } 166 | 167 | fileprivate final class Demo_NewsServicesubscribeSessionBase: ServerSessionServerStreamingBase, Demo_NewsServicesubscribeSession {} 168 | 169 | internal protocol Demo_NewsServicepostNewsSession: ServerSessionUnary {} 170 | 171 | fileprivate final class Demo_NewsServicepostNewsSessionBase: ServerSessionUnaryBase, Demo_NewsServicepostNewsSession {} 172 | 173 | -------------------------------------------------------------------------------- /ios/ios/news_service.pb.swift: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | // 3 | // Generated by the Swift generator plugin for the protocol buffer compiler. 4 | // Source: news_service.proto 5 | // 6 | // For information on using the generated types, please see the documenation: 7 | // https://github.com/apple/swift-protobuf/ 8 | 9 | import Foundation 10 | import SwiftProtobuf 11 | 12 | // If the compiler emits an error on this type, it is because this file 13 | // was generated by a version of the `protoc` Swift plug-in that is 14 | // incompatible with the version of SwiftProtobuf to which you are linking. 15 | // Please ensure that your are building against the same version of the API 16 | // that was used to generate this file. 17 | fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 18 | struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 19 | typealias Version = _2 20 | } 21 | 22 | enum Demo_Topic: SwiftProtobuf.Enum { 23 | typealias RawValue = Int 24 | case tech // = 0 25 | case sport // = 1 26 | case economy // = 2 27 | case UNRECOGNIZED(Int) 28 | 29 | init() { 30 | self = .tech 31 | } 32 | 33 | init?(rawValue: Int) { 34 | switch rawValue { 35 | case 0: self = .tech 36 | case 1: self = .sport 37 | case 2: self = .economy 38 | default: self = .UNRECOGNIZED(rawValue) 39 | } 40 | } 41 | 42 | var rawValue: Int { 43 | switch self { 44 | case .tech: return 0 45 | case .sport: return 1 46 | case .economy: return 2 47 | case .UNRECOGNIZED(let i): return i 48 | } 49 | } 50 | 51 | } 52 | 53 | #if swift(>=4.2) 54 | 55 | extension Demo_Topic: CaseIterable { 56 | // The compiler won't synthesize support with the UNRECOGNIZED case. 57 | static var allCases: [Demo_Topic] = [ 58 | .tech, 59 | .sport, 60 | .economy, 61 | ] 62 | } 63 | 64 | #endif // swift(>=4.2) 65 | 66 | struct Demo_NewsRequest { 67 | // SwiftProtobuf.Message conformance is added in an extension below. See the 68 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 69 | // methods supported on all messages. 70 | 71 | var topic: Demo_Topic = .tech 72 | 73 | var unknownFields = SwiftProtobuf.UnknownStorage() 74 | 75 | init() {} 76 | } 77 | 78 | struct Demo_SubscribeRequest { 79 | // SwiftProtobuf.Message conformance is added in an extension below. See the 80 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 81 | // methods supported on all messages. 82 | 83 | var topic: Demo_Topic = .tech 84 | 85 | var unknownFields = SwiftProtobuf.UnknownStorage() 86 | 87 | init() {} 88 | } 89 | 90 | struct Demo_NewsResponse { 91 | // SwiftProtobuf.Message conformance is added in an extension below. See the 92 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 93 | // methods supported on all messages. 94 | 95 | var news: [Demo_News] = [] 96 | 97 | var unknownFields = SwiftProtobuf.UnknownStorage() 98 | 99 | init() {} 100 | } 101 | 102 | struct Demo_News { 103 | // SwiftProtobuf.Message conformance is added in an extension below. See the 104 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 105 | // methods supported on all messages. 106 | 107 | var newsID: String = String() 108 | 109 | var title: String = String() 110 | 111 | var imageURL: String = String() 112 | 113 | var description_p: String = String() 114 | 115 | var topic: Demo_Topic = .tech 116 | 117 | var unknownFields = SwiftProtobuf.UnknownStorage() 118 | 119 | init() {} 120 | } 121 | 122 | // MARK: - Code below here is support for the SwiftProtobuf runtime. 123 | 124 | fileprivate let _protobuf_package = "demo" 125 | 126 | extension Demo_Topic: SwiftProtobuf._ProtoNameProviding { 127 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 128 | 0: .same(proto: "TECH"), 129 | 1: .same(proto: "SPORT"), 130 | 2: .same(proto: "ECONOMY"), 131 | ] 132 | } 133 | 134 | extension Demo_NewsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 135 | static let protoMessageName: String = _protobuf_package + ".NewsRequest" 136 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 137 | 1: .same(proto: "topic"), 138 | ] 139 | 140 | mutating func decodeMessage(decoder: inout D) throws { 141 | while let fieldNumber = try decoder.nextFieldNumber() { 142 | switch fieldNumber { 143 | case 1: try decoder.decodeSingularEnumField(value: &self.topic) 144 | default: break 145 | } 146 | } 147 | } 148 | 149 | func traverse(visitor: inout V) throws { 150 | if self.topic != .tech { 151 | try visitor.visitSingularEnumField(value: self.topic, fieldNumber: 1) 152 | } 153 | try unknownFields.traverse(visitor: &visitor) 154 | } 155 | 156 | static func ==(lhs: Demo_NewsRequest, rhs: Demo_NewsRequest) -> Bool { 157 | if lhs.topic != rhs.topic {return false} 158 | if lhs.unknownFields != rhs.unknownFields {return false} 159 | return true 160 | } 161 | } 162 | 163 | extension Demo_SubscribeRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 164 | static let protoMessageName: String = _protobuf_package + ".SubscribeRequest" 165 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 166 | 1: .same(proto: "topic"), 167 | ] 168 | 169 | mutating func decodeMessage(decoder: inout D) throws { 170 | while let fieldNumber = try decoder.nextFieldNumber() { 171 | switch fieldNumber { 172 | case 1: try decoder.decodeSingularEnumField(value: &self.topic) 173 | default: break 174 | } 175 | } 176 | } 177 | 178 | func traverse(visitor: inout V) throws { 179 | if self.topic != .tech { 180 | try visitor.visitSingularEnumField(value: self.topic, fieldNumber: 1) 181 | } 182 | try unknownFields.traverse(visitor: &visitor) 183 | } 184 | 185 | static func ==(lhs: Demo_SubscribeRequest, rhs: Demo_SubscribeRequest) -> Bool { 186 | if lhs.topic != rhs.topic {return false} 187 | if lhs.unknownFields != rhs.unknownFields {return false} 188 | return true 189 | } 190 | } 191 | 192 | extension Demo_NewsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 193 | static let protoMessageName: String = _protobuf_package + ".NewsResponse" 194 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 195 | 1: .same(proto: "news"), 196 | ] 197 | 198 | mutating func decodeMessage(decoder: inout D) throws { 199 | while let fieldNumber = try decoder.nextFieldNumber() { 200 | switch fieldNumber { 201 | case 1: try decoder.decodeRepeatedMessageField(value: &self.news) 202 | default: break 203 | } 204 | } 205 | } 206 | 207 | func traverse(visitor: inout V) throws { 208 | if !self.news.isEmpty { 209 | try visitor.visitRepeatedMessageField(value: self.news, fieldNumber: 1) 210 | } 211 | try unknownFields.traverse(visitor: &visitor) 212 | } 213 | 214 | static func ==(lhs: Demo_NewsResponse, rhs: Demo_NewsResponse) -> Bool { 215 | if lhs.news != rhs.news {return false} 216 | if lhs.unknownFields != rhs.unknownFields {return false} 217 | return true 218 | } 219 | } 220 | 221 | extension Demo_News: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 222 | static let protoMessageName: String = _protobuf_package + ".News" 223 | static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 224 | 1: .same(proto: "newsId"), 225 | 2: .same(proto: "title"), 226 | 3: .same(proto: "imageUrl"), 227 | 4: .same(proto: "description"), 228 | 5: .same(proto: "topic"), 229 | ] 230 | 231 | mutating func decodeMessage(decoder: inout D) throws { 232 | while let fieldNumber = try decoder.nextFieldNumber() { 233 | switch fieldNumber { 234 | case 1: try decoder.decodeSingularStringField(value: &self.newsID) 235 | case 2: try decoder.decodeSingularStringField(value: &self.title) 236 | case 3: try decoder.decodeSingularStringField(value: &self.imageURL) 237 | case 4: try decoder.decodeSingularStringField(value: &self.description_p) 238 | case 5: try decoder.decodeSingularEnumField(value: &self.topic) 239 | default: break 240 | } 241 | } 242 | } 243 | 244 | func traverse(visitor: inout V) throws { 245 | if !self.newsID.isEmpty { 246 | try visitor.visitSingularStringField(value: self.newsID, fieldNumber: 1) 247 | } 248 | if !self.title.isEmpty { 249 | try visitor.visitSingularStringField(value: self.title, fieldNumber: 2) 250 | } 251 | if !self.imageURL.isEmpty { 252 | try visitor.visitSingularStringField(value: self.imageURL, fieldNumber: 3) 253 | } 254 | if !self.description_p.isEmpty { 255 | try visitor.visitSingularStringField(value: self.description_p, fieldNumber: 4) 256 | } 257 | if self.topic != .tech { 258 | try visitor.visitSingularEnumField(value: self.topic, fieldNumber: 5) 259 | } 260 | try unknownFields.traverse(visitor: &visitor) 261 | } 262 | 263 | static func ==(lhs: Demo_News, rhs: Demo_News) -> Bool { 264 | if lhs.newsID != rhs.newsID {return false} 265 | if lhs.title != rhs.title {return false} 266 | if lhs.imageURL != rhs.imageURL {return false} 267 | if lhs.description_p != rhs.description_p {return false} 268 | if lhs.topic != rhs.topic {return false} 269 | if lhs.unknownFields != rhs.unknownFields {return false} 270 | return true 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /newscli/README.md: -------------------------------------------------------------------------------- 1 | # gRPC Client (cli) example 2 | 3 | ## Build & Run client 4 | 5 | Project require Go installed. 6 | 7 | 1. install newscli : 8 | ```SH 9 | go install github.com/fb64/grpc-fullstack-demo/newscli@latest 10 | ``` 11 | 12 | 2. Start server (see [backend project](../backends/kotlin-graal)) 13 | 14 | 3. run newscli : 15 | ``` 16 | newscli -h 17 | ``` 18 | 19 | 20 | ## Libraries used 21 | 22 | * [urfave cli](https://github.com/urfave/cl) 23 | -------------------------------------------------------------------------------- /newscli/client_news.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | pb "github.com/fb64/grpc-fullstack-demo/newscli/news_service" 11 | "google.golang.org/grpc" 12 | "gopkg.in/urfave/cli.v1" 13 | ) 14 | 15 | var client pb.NewsServiceClient 16 | var connection *grpc.ClientConn 17 | var connectionError error 18 | 19 | func main() { 20 | 21 | var topicString string 22 | var address string 23 | 24 | defer closeConnection() 25 | 26 | app := cli.NewApp() 27 | app.Name = "client_news" 28 | app.Usage = "news grpc client (command line interface)" 29 | app.Version = "1.0.0" 30 | 31 | app.Flags = []cli.Flag{ 32 | cli.StringFlag{ 33 | Name: "topic, t", 34 | Value: "TECH", 35 | Usage: "Topic to use : TECH, SPOR, ECONOMY", 36 | Destination: &topicString, 37 | }, 38 | cli.StringFlag{ 39 | Name: "address, a", 40 | Value: "localhost:6565", 41 | Usage: "address of the news server", 42 | Destination: &address, 43 | }, 44 | } 45 | 46 | app.Commands = []cli.Command{ 47 | { 48 | Name: "list", 49 | Usage: "list news of a topic", 50 | Action: func(c *cli.Context) error { 51 | initClient(address) 52 | topic := stringToTopic(topicString) 53 | listNews(client, topic) 54 | return nil 55 | }, 56 | }, 57 | { 58 | Name: "add", 59 | Usage: "add a news in a topic", 60 | Action: func(c *cli.Context) error { 61 | initClient(address) 62 | topic := stringToTopic(topicString) 63 | postNews(client, &pb.News{Title: "Title From GO", ImageUrl: "https://picsum.photos/400/400/?random", Description: "Description From GO", Topic: topic}) 64 | return nil 65 | }, 66 | }, 67 | { 68 | Name: "subscribe", 69 | Usage: "subscribe to a news's topic for 30s", 70 | Action: func(c *cli.Context) error { 71 | initClient(address) 72 | topic := stringToTopic(topicString) 73 | subscribeTo(client, topic) 74 | return nil 75 | }, 76 | }, 77 | } 78 | 79 | appErr := app.Run(os.Args) 80 | if appErr != nil { 81 | log.Fatal(appErr) 82 | } 83 | } 84 | 85 | func initClient(serverAddress string) { 86 | connection, connectionError = grpc.Dial(serverAddress, grpc.WithInsecure()) 87 | if connectionError != nil { 88 | log.Fatalf("did not connect: %v", connectionError) 89 | } 90 | client = pb.NewNewsServiceClient(connection) 91 | } 92 | 93 | func closeConnection() { 94 | if connection != nil { 95 | connection.Close() 96 | } 97 | 98 | } 99 | 100 | func stringToTopic(topicString string) pb.Topic { 101 | topicValue := pb.Topic_value[topicString] 102 | return pb.Topic(topicValue) 103 | } 104 | 105 | func listNews(client pb.NewsServiceClient, topic pb.Topic) { 106 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 107 | defer cancel() 108 | 109 | r, err := client.GetNews(ctx, &pb.NewsRequest{Topic: topic}) 110 | if err != nil { 111 | log.Fatalf("could get news: %v", err) 112 | } 113 | log.Printf("%s", r.News) 114 | } 115 | 116 | func subscribeTo(client pb.NewsServiceClient, topic pb.Topic) { 117 | 118 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 119 | defer cancel() 120 | 121 | stream, err := client.Subscribe(ctx, &pb.SubscribeRequest{Topic: pb.Topic_TECH}) 122 | if err != nil { 123 | log.Fatalf("could subscribe: %v", err) 124 | } 125 | 126 | waitc := make(chan struct{}) 127 | go func() { 128 | for { 129 | news, err := stream.Recv() 130 | if err == io.EOF { 131 | close(waitc) 132 | return 133 | } 134 | if err != nil { 135 | log.Fatalf("%v.Subscribe(_) = _, %v", client, err) 136 | } 137 | log.Println(news) 138 | } 139 | }() 140 | <-waitc 141 | } 142 | 143 | func postNews(client pb.NewsServiceClient, newsToAdd *pb.News) { 144 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 145 | defer cancel() 146 | 147 | newsSaved, err := client.PostNews(ctx, newsToAdd) 148 | if err != nil { 149 | log.Fatalf("could post news: %v", err) 150 | } 151 | log.Printf("news saved: %s", newsSaved) 152 | } 153 | -------------------------------------------------------------------------------- /newscli/news_service/news_service.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: news_service.proto 3 | 4 | package demo 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | import ( 11 | context "golang.org/x/net/context" 12 | grpc "google.golang.org/grpc" 13 | ) 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the proto package it is being compiled against. 22 | // A compilation error at this line likely means your copy of the 23 | // proto package needs to be updated. 24 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 25 | 26 | type Topic int32 27 | 28 | const ( 29 | Topic_TECH Topic = 0 30 | Topic_SPORT Topic = 1 31 | Topic_ECONOMY Topic = 2 32 | ) 33 | 34 | var Topic_name = map[int32]string{ 35 | 0: "TECH", 36 | 1: "SPORT", 37 | 2: "ECONOMY", 38 | } 39 | 40 | var Topic_value = map[string]int32{ 41 | "TECH": 0, 42 | "SPORT": 1, 43 | "ECONOMY": 2, 44 | } 45 | 46 | func (x Topic) String() string { 47 | return proto.EnumName(Topic_name, int32(x)) 48 | } 49 | 50 | func (Topic) EnumDescriptor() ([]byte, []int) { 51 | return fileDescriptor_8ac5cc3c062ad010, []int{0} 52 | } 53 | 54 | type NewsRequest struct { 55 | Topic Topic `protobuf:"varint,1,opt,name=topic,proto3,enum=demo.Topic" json:"topic,omitempty"` 56 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 57 | XXX_unrecognized []byte `json:"-"` 58 | XXX_sizecache int32 `json:"-"` 59 | } 60 | 61 | func (m *NewsRequest) Reset() { *m = NewsRequest{} } 62 | func (m *NewsRequest) String() string { return proto.CompactTextString(m) } 63 | func (*NewsRequest) ProtoMessage() {} 64 | func (*NewsRequest) Descriptor() ([]byte, []int) { 65 | return fileDescriptor_8ac5cc3c062ad010, []int{0} 66 | } 67 | func (m *NewsRequest) XXX_Unmarshal(b []byte) error { 68 | return xxx_messageInfo_NewsRequest.Unmarshal(m, b) 69 | } 70 | func (m *NewsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 71 | return xxx_messageInfo_NewsRequest.Marshal(b, m, deterministic) 72 | } 73 | func (m *NewsRequest) XXX_Merge(src proto.Message) { 74 | xxx_messageInfo_NewsRequest.Merge(m, src) 75 | } 76 | func (m *NewsRequest) XXX_Size() int { 77 | return xxx_messageInfo_NewsRequest.Size(m) 78 | } 79 | func (m *NewsRequest) XXX_DiscardUnknown() { 80 | xxx_messageInfo_NewsRequest.DiscardUnknown(m) 81 | } 82 | 83 | var xxx_messageInfo_NewsRequest proto.InternalMessageInfo 84 | 85 | func (m *NewsRequest) GetTopic() Topic { 86 | if m != nil { 87 | return m.Topic 88 | } 89 | return Topic_TECH 90 | } 91 | 92 | type SubscribeRequest struct { 93 | Topic Topic `protobuf:"varint,1,opt,name=topic,proto3,enum=demo.Topic" json:"topic,omitempty"` 94 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 95 | XXX_unrecognized []byte `json:"-"` 96 | XXX_sizecache int32 `json:"-"` 97 | } 98 | 99 | func (m *SubscribeRequest) Reset() { *m = SubscribeRequest{} } 100 | func (m *SubscribeRequest) String() string { return proto.CompactTextString(m) } 101 | func (*SubscribeRequest) ProtoMessage() {} 102 | func (*SubscribeRequest) Descriptor() ([]byte, []int) { 103 | return fileDescriptor_8ac5cc3c062ad010, []int{1} 104 | } 105 | func (m *SubscribeRequest) XXX_Unmarshal(b []byte) error { 106 | return xxx_messageInfo_SubscribeRequest.Unmarshal(m, b) 107 | } 108 | func (m *SubscribeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 109 | return xxx_messageInfo_SubscribeRequest.Marshal(b, m, deterministic) 110 | } 111 | func (m *SubscribeRequest) XXX_Merge(src proto.Message) { 112 | xxx_messageInfo_SubscribeRequest.Merge(m, src) 113 | } 114 | func (m *SubscribeRequest) XXX_Size() int { 115 | return xxx_messageInfo_SubscribeRequest.Size(m) 116 | } 117 | func (m *SubscribeRequest) XXX_DiscardUnknown() { 118 | xxx_messageInfo_SubscribeRequest.DiscardUnknown(m) 119 | } 120 | 121 | var xxx_messageInfo_SubscribeRequest proto.InternalMessageInfo 122 | 123 | func (m *SubscribeRequest) GetTopic() Topic { 124 | if m != nil { 125 | return m.Topic 126 | } 127 | return Topic_TECH 128 | } 129 | 130 | type NewsResponse struct { 131 | News []*News `protobuf:"bytes,1,rep,name=news,proto3" json:"news,omitempty"` 132 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 133 | XXX_unrecognized []byte `json:"-"` 134 | XXX_sizecache int32 `json:"-"` 135 | } 136 | 137 | func (m *NewsResponse) Reset() { *m = NewsResponse{} } 138 | func (m *NewsResponse) String() string { return proto.CompactTextString(m) } 139 | func (*NewsResponse) ProtoMessage() {} 140 | func (*NewsResponse) Descriptor() ([]byte, []int) { 141 | return fileDescriptor_8ac5cc3c062ad010, []int{2} 142 | } 143 | func (m *NewsResponse) XXX_Unmarshal(b []byte) error { 144 | return xxx_messageInfo_NewsResponse.Unmarshal(m, b) 145 | } 146 | func (m *NewsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 147 | return xxx_messageInfo_NewsResponse.Marshal(b, m, deterministic) 148 | } 149 | func (m *NewsResponse) XXX_Merge(src proto.Message) { 150 | xxx_messageInfo_NewsResponse.Merge(m, src) 151 | } 152 | func (m *NewsResponse) XXX_Size() int { 153 | return xxx_messageInfo_NewsResponse.Size(m) 154 | } 155 | func (m *NewsResponse) XXX_DiscardUnknown() { 156 | xxx_messageInfo_NewsResponse.DiscardUnknown(m) 157 | } 158 | 159 | var xxx_messageInfo_NewsResponse proto.InternalMessageInfo 160 | 161 | func (m *NewsResponse) GetNews() []*News { 162 | if m != nil { 163 | return m.News 164 | } 165 | return nil 166 | } 167 | 168 | type News struct { 169 | NewsId string `protobuf:"bytes,1,opt,name=newsId,proto3" json:"newsId,omitempty"` 170 | Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` 171 | ImageUrl string `protobuf:"bytes,3,opt,name=imageUrl,proto3" json:"imageUrl,omitempty"` 172 | Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` 173 | Topic Topic `protobuf:"varint,5,opt,name=topic,proto3,enum=demo.Topic" json:"topic,omitempty"` 174 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 175 | XXX_unrecognized []byte `json:"-"` 176 | XXX_sizecache int32 `json:"-"` 177 | } 178 | 179 | func (m *News) Reset() { *m = News{} } 180 | func (m *News) String() string { return proto.CompactTextString(m) } 181 | func (*News) ProtoMessage() {} 182 | func (*News) Descriptor() ([]byte, []int) { 183 | return fileDescriptor_8ac5cc3c062ad010, []int{3} 184 | } 185 | func (m *News) XXX_Unmarshal(b []byte) error { 186 | return xxx_messageInfo_News.Unmarshal(m, b) 187 | } 188 | func (m *News) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 189 | return xxx_messageInfo_News.Marshal(b, m, deterministic) 190 | } 191 | func (m *News) XXX_Merge(src proto.Message) { 192 | xxx_messageInfo_News.Merge(m, src) 193 | } 194 | func (m *News) XXX_Size() int { 195 | return xxx_messageInfo_News.Size(m) 196 | } 197 | func (m *News) XXX_DiscardUnknown() { 198 | xxx_messageInfo_News.DiscardUnknown(m) 199 | } 200 | 201 | var xxx_messageInfo_News proto.InternalMessageInfo 202 | 203 | func (m *News) GetNewsId() string { 204 | if m != nil { 205 | return m.NewsId 206 | } 207 | return "" 208 | } 209 | 210 | func (m *News) GetTitle() string { 211 | if m != nil { 212 | return m.Title 213 | } 214 | return "" 215 | } 216 | 217 | func (m *News) GetImageUrl() string { 218 | if m != nil { 219 | return m.ImageUrl 220 | } 221 | return "" 222 | } 223 | 224 | func (m *News) GetDescription() string { 225 | if m != nil { 226 | return m.Description 227 | } 228 | return "" 229 | } 230 | 231 | func (m *News) GetTopic() Topic { 232 | if m != nil { 233 | return m.Topic 234 | } 235 | return Topic_TECH 236 | } 237 | 238 | func init() { 239 | proto.RegisterType((*NewsRequest)(nil), "demo.NewsRequest") 240 | proto.RegisterType((*SubscribeRequest)(nil), "demo.SubscribeRequest") 241 | proto.RegisterType((*NewsResponse)(nil), "demo.NewsResponse") 242 | proto.RegisterType((*News)(nil), "demo.News") 243 | proto.RegisterEnum("demo.Topic", Topic_name, Topic_value) 244 | } 245 | 246 | // Reference imports to suppress errors if they are not otherwise used. 247 | var _ context.Context 248 | var _ grpc.ClientConn 249 | 250 | // This is a compile-time assertion to ensure that this generated file 251 | // is compatible with the grpc package it is being compiled against. 252 | const _ = grpc.SupportPackageIsVersion4 253 | 254 | // NewsServiceClient is the client API for NewsService service. 255 | // 256 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 257 | type NewsServiceClient interface { 258 | GetNews(ctx context.Context, in *NewsRequest, opts ...grpc.CallOption) (*NewsResponse, error) 259 | Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (NewsService_SubscribeClient, error) 260 | PostNews(ctx context.Context, in *News, opts ...grpc.CallOption) (*News, error) 261 | } 262 | 263 | type newsServiceClient struct { 264 | cc *grpc.ClientConn 265 | } 266 | 267 | func NewNewsServiceClient(cc *grpc.ClientConn) NewsServiceClient { 268 | return &newsServiceClient{cc} 269 | } 270 | 271 | func (c *newsServiceClient) GetNews(ctx context.Context, in *NewsRequest, opts ...grpc.CallOption) (*NewsResponse, error) { 272 | out := new(NewsResponse) 273 | err := c.cc.Invoke(ctx, "/demo.NewsService/getNews", in, out, opts...) 274 | if err != nil { 275 | return nil, err 276 | } 277 | return out, nil 278 | } 279 | 280 | func (c *newsServiceClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (NewsService_SubscribeClient, error) { 281 | stream, err := c.cc.NewStream(ctx, &_NewsService_serviceDesc.Streams[0], "/demo.NewsService/subscribe", opts...) 282 | if err != nil { 283 | return nil, err 284 | } 285 | x := &newsServiceSubscribeClient{stream} 286 | if err := x.ClientStream.SendMsg(in); err != nil { 287 | return nil, err 288 | } 289 | if err := x.ClientStream.CloseSend(); err != nil { 290 | return nil, err 291 | } 292 | return x, nil 293 | } 294 | 295 | type NewsService_SubscribeClient interface { 296 | Recv() (*News, error) 297 | grpc.ClientStream 298 | } 299 | 300 | type newsServiceSubscribeClient struct { 301 | grpc.ClientStream 302 | } 303 | 304 | func (x *newsServiceSubscribeClient) Recv() (*News, error) { 305 | m := new(News) 306 | if err := x.ClientStream.RecvMsg(m); err != nil { 307 | return nil, err 308 | } 309 | return m, nil 310 | } 311 | 312 | func (c *newsServiceClient) PostNews(ctx context.Context, in *News, opts ...grpc.CallOption) (*News, error) { 313 | out := new(News) 314 | err := c.cc.Invoke(ctx, "/demo.NewsService/postNews", in, out, opts...) 315 | if err != nil { 316 | return nil, err 317 | } 318 | return out, nil 319 | } 320 | 321 | // NewsServiceServer is the server API for NewsService service. 322 | type NewsServiceServer interface { 323 | GetNews(context.Context, *NewsRequest) (*NewsResponse, error) 324 | Subscribe(*SubscribeRequest, NewsService_SubscribeServer) error 325 | PostNews(context.Context, *News) (*News, error) 326 | } 327 | 328 | func RegisterNewsServiceServer(s *grpc.Server, srv NewsServiceServer) { 329 | s.RegisterService(&_NewsService_serviceDesc, srv) 330 | } 331 | 332 | func _NewsService_GetNews_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 333 | in := new(NewsRequest) 334 | if err := dec(in); err != nil { 335 | return nil, err 336 | } 337 | if interceptor == nil { 338 | return srv.(NewsServiceServer).GetNews(ctx, in) 339 | } 340 | info := &grpc.UnaryServerInfo{ 341 | Server: srv, 342 | FullMethod: "/demo.NewsService/GetNews", 343 | } 344 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 345 | return srv.(NewsServiceServer).GetNews(ctx, req.(*NewsRequest)) 346 | } 347 | return interceptor(ctx, in, info, handler) 348 | } 349 | 350 | func _NewsService_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { 351 | m := new(SubscribeRequest) 352 | if err := stream.RecvMsg(m); err != nil { 353 | return err 354 | } 355 | return srv.(NewsServiceServer).Subscribe(m, &newsServiceSubscribeServer{stream}) 356 | } 357 | 358 | type NewsService_SubscribeServer interface { 359 | Send(*News) error 360 | grpc.ServerStream 361 | } 362 | 363 | type newsServiceSubscribeServer struct { 364 | grpc.ServerStream 365 | } 366 | 367 | func (x *newsServiceSubscribeServer) Send(m *News) error { 368 | return x.ServerStream.SendMsg(m) 369 | } 370 | 371 | func _NewsService_PostNews_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 372 | in := new(News) 373 | if err := dec(in); err != nil { 374 | return nil, err 375 | } 376 | if interceptor == nil { 377 | return srv.(NewsServiceServer).PostNews(ctx, in) 378 | } 379 | info := &grpc.UnaryServerInfo{ 380 | Server: srv, 381 | FullMethod: "/demo.NewsService/PostNews", 382 | } 383 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 384 | return srv.(NewsServiceServer).PostNews(ctx, req.(*News)) 385 | } 386 | return interceptor(ctx, in, info, handler) 387 | } 388 | 389 | var _NewsService_serviceDesc = grpc.ServiceDesc{ 390 | ServiceName: "demo.NewsService", 391 | HandlerType: (*NewsServiceServer)(nil), 392 | Methods: []grpc.MethodDesc{ 393 | { 394 | MethodName: "getNews", 395 | Handler: _NewsService_GetNews_Handler, 396 | }, 397 | { 398 | MethodName: "postNews", 399 | Handler: _NewsService_PostNews_Handler, 400 | }, 401 | }, 402 | Streams: []grpc.StreamDesc{ 403 | { 404 | StreamName: "subscribe", 405 | Handler: _NewsService_Subscribe_Handler, 406 | ServerStreams: true, 407 | }, 408 | }, 409 | Metadata: "news_service.proto", 410 | } 411 | 412 | func init() { proto.RegisterFile("news_service.proto", fileDescriptor_8ac5cc3c062ad010) } 413 | 414 | var fileDescriptor_8ac5cc3c062ad010 = []byte{ 415 | // 346 bytes of a gzipped FileDescriptorProto 416 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0x4f, 0x6b, 0xea, 0x40, 417 | 0x14, 0xc5, 0x1d, 0x4d, 0xfc, 0x73, 0xf3, 0x78, 0xe4, 0x5d, 0xde, 0x93, 0xe0, 0xe2, 0x61, 0x43, 418 | 0x17, 0xb6, 0x85, 0x20, 0x91, 0x7e, 0x01, 0x45, 0x68, 0x17, 0x55, 0x89, 0x76, 0xd1, 0x55, 0x31, 419 | 0xc9, 0x55, 0x02, 0x9a, 0xa4, 0x33, 0x63, 0xfd, 0x24, 0xdd, 0xf5, 0xc3, 0x96, 0x99, 0xe9, 0x9f, 420 | 0xb4, 0xd0, 0x45, 0x97, 0xe7, 0xdc, 0x33, 0x33, 0xf7, 0xfc, 0x18, 0xc0, 0x9c, 0x8e, 0xe2, 0x5e, 421 | 0x10, 0x7f, 0xcc, 0x12, 0x0a, 0x4a, 0x5e, 0xc8, 0x02, 0xad, 0x94, 0xf6, 0x85, 0x3f, 0x04, 0x67, 422 | 0x46, 0x47, 0x11, 0xd1, 0xc3, 0x81, 0x84, 0xc4, 0x13, 0xb0, 0x65, 0x51, 0x66, 0x89, 0xc7, 0xfa, 423 | 0x6c, 0xf0, 0x3b, 0x74, 0x02, 0x15, 0x0a, 0x56, 0xca, 0x8a, 0xcc, 0xc4, 0xbf, 0x04, 0x77, 0x79, 424 | 0x88, 0x45, 0xc2, 0xb3, 0x98, 0x7e, 0x70, 0x2c, 0x80, 0x5f, 0xe6, 0x21, 0x51, 0x16, 0xb9, 0x20, 425 | 0xfc, 0x0f, 0x96, 0x5a, 0xca, 0x63, 0xfd, 0xc6, 0xc0, 0x09, 0xc1, 0x9c, 0xd0, 0x09, 0xed, 0xfb, 426 | 0x4f, 0x0c, 0x2c, 0x25, 0xb1, 0x0b, 0x4d, 0x65, 0x5c, 0xa7, 0xfa, 0xf2, 0x4e, 0xf4, 0xaa, 0xf0, 427 | 0x2f, 0xd8, 0x32, 0x93, 0x3b, 0xf2, 0xea, 0xda, 0x36, 0x02, 0x7b, 0xd0, 0xce, 0xf6, 0xeb, 0x2d, 428 | 0xdd, 0xf2, 0x9d, 0xd7, 0xd0, 0x83, 0x77, 0x8d, 0x7d, 0x70, 0x52, 0x52, 0x8b, 0x97, 0x32, 0x2b, 429 | 0x72, 0xcf, 0xd2, 0xe3, 0xaa, 0xf5, 0xd1, 0xc3, 0xfe, 0xae, 0xc7, 0xf9, 0x19, 0xd8, 0x5a, 0x63, 430 | 0x1b, 0xac, 0xd5, 0x74, 0x72, 0xe5, 0xd6, 0xb0, 0x03, 0xf6, 0x72, 0x31, 0x8f, 0x56, 0x2e, 0x43, 431 | 0x07, 0x5a, 0xd3, 0xc9, 0x7c, 0x36, 0xbf, 0xb9, 0x73, 0xeb, 0xe1, 0x33, 0x33, 0x70, 0x97, 0x86, 432 | 0x3b, 0x86, 0xd0, 0xda, 0x92, 0xd4, 0xa5, 0xfe, 0x54, 0xfa, 0x1a, 0x86, 0x3d, 0xac, 0x5a, 0x06, 433 | 0x92, 0x5f, 0xc3, 0x11, 0x74, 0xc4, 0x1b, 0x6d, 0xec, 0x9a, 0xc8, 0x57, 0xfc, 0xbd, 0x0a, 0x3d, 434 | 0xbf, 0x36, 0x64, 0x78, 0x0a, 0xed, 0xb2, 0x10, 0xe6, 0xa5, 0xca, 0xec, 0x73, 0x6e, 0x7c, 0x01, 435 | 0xff, 0x36, 0x3c, 0xd8, 0xc4, 0xc4, 0xf3, 0x35, 0x4f, 0x83, 0x2d, 0x2f, 0x93, 0x40, 0xb1, 0x1d, 436 | 0xbb, 0x95, 0xa5, 0x17, 0xea, 0xaf, 0x2c, 0x58, 0xdc, 0xd4, 0x9f, 0x66, 0xf4, 0x12, 0x00, 0x00, 437 | 0xff, 0xff, 0x3f, 0x52, 0x2d, 0xd6, 0x4a, 0x02, 0x00, 0x00, 438 | } 439 | -------------------------------------------------------------------------------- /protos/news_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | 5 | option java_package = "fr.fbernard.grpc.news"; 6 | 7 | option java_outer_classname = "NewsServiceProto"; 8 | 9 | package demo; 10 | 11 | 12 | //Services 13 | service NewsService { 14 | rpc getNews(NewsRequest) returns (NewsResponse) {} 15 | rpc subscribe(SubscribeRequest) returns (stream News) {} 16 | rpc postNews (News) returns (News) {} 17 | } 18 | 19 | 20 | message NewsRequest{ 21 | Topic topic = 1; 22 | } 23 | 24 | message SubscribeRequest{ 25 | Topic topic = 1; 26 | } 27 | 28 | message NewsResponse{ 29 | repeated News news = 1; 30 | } 31 | 32 | message News{ 33 | string newsId = 1; 34 | string title = 2; 35 | string imageUrl = 3; 36 | string description = 4; 37 | Topic topic = 5; 38 | } 39 | 40 | 41 | enum Topic{ 42 | TECH = 0; 43 | SPORT = 1; 44 | ECONOMY = 2; 45 | } 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /web/.eslintignore: -------------------------------------------------------------------------------- 1 | store/grpc 2 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | '@nuxtjs', 12 | 'plugin:prettier/recommended' 13 | ], 14 | plugins: [ 15 | 'prettier' 16 | ], 17 | // add your custom rules here 18 | rules: {} 19 | } 20 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # news-webapp 2 | 3 | 4 | 5 | 6 | > News webapp with grpc-web 7 | 8 | 9 | ## Build and Run 10 | 11 | Javascript client files have been generated with `protoc` : https://grpc.io/docs/platforms/web/basics/#generate-protobuf-messages-and-service-client-stub 12 | 13 | 14 | ``` bash 15 | # install dependencies 16 | $ npm install 17 | 18 | # serve with hot reload at localhost:3000 19 | $ npm run dev 20 | 21 | # build for production and launch server 22 | $ npm run build 23 | $ npm start 24 | 25 | # generate static project 26 | $ npm run generate 27 | ``` 28 | 29 | For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org). 30 | 31 | ## Used Stack 32 | * [NuxtJS](https://nuxtjs.org/) 33 | * [Bootstrap-Vue](https://bootstrap-vue.js.org/) 34 | * [grpc-web](https://github.com/grpc/grpc-web) 35 | * [grpc-web-middleware](https://github.com/fb64/grpc-web-middleware) 36 | -------------------------------------------------------------------------------- /web/assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /web/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 80 | -------------------------------------------------------------------------------- /web/components/News.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 39 | 40 | 41 | 97 | -------------------------------------------------------------------------------- /web/components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /web/layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /web/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 56 | -------------------------------------------------------------------------------- /web/middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /web/nuxt.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package') 2 | 3 | module.exports = { 4 | mode: 'universal', 5 | /* 6 | ** Headers of the page 7 | */ 8 | head: { 9 | title: pkg.name, 10 | meta: [ 11 | { charset: 'utf-8' }, 12 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 13 | { hid: 'description', name: 'description', content: pkg.description } 14 | ], 15 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }] 16 | }, 17 | 18 | /* 19 | ** Customize the progress-bar color 20 | */ 21 | loading: { color: '#fff' }, 22 | 23 | /* 24 | ** Global CSS 25 | */ 26 | css: [], 27 | 28 | /* 29 | ** Plugins to load before mounting the App 30 | */ 31 | plugins: [], 32 | 33 | /* 34 | ** Nuxt.js modules 35 | */ 36 | modules: [ 37 | // Doc: https://bootstrap-vue.js.org/docs/ 38 | 'bootstrap-vue/nuxt' 39 | ], 40 | 41 | /* 42 | ** Build configuration 43 | */ 44 | build: { 45 | /* 46 | ** You can extend webpack config here 47 | */ 48 | extend(config, ctx) { 49 | // Run ESLint on save 50 | if (ctx.isDev && ctx.isClient) { 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "news-webapp", 3 | "version": "1.0.0", 4 | "description": "News webapp with grpc-web", 5 | "author": "fbernard", 6 | "private": true, 7 | "scripts": { 8 | "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server", 9 | "build": "nuxt build", 10 | "start": "cross-env NODE_ENV=production node server/index.js", 11 | "generate": "nuxt generate", 12 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 13 | "precommit": "npm run lint" 14 | }, 15 | "dependencies": { 16 | "@koa/cors": "^5.0.0", 17 | "bootstrap": "^4.6.2", 18 | "bootstrap-vue": "^2.23.0", 19 | "cross-env": "^5.2.0", 20 | "google-protobuf": "^3.21.2", 21 | "grpc-web": "^1.5.0", 22 | "grpc-web-middleware": "^0.2.3", 23 | "koa": "^2.15.0", 24 | "nuxt": "^2.17" 25 | }, 26 | "devDependencies": { 27 | "@babel/eslint-parser": "^7.19.1", 28 | "@nuxtjs/eslint-config": "^11.0.0", 29 | "@nuxtjs/eslint-module": "^3.1.0", 30 | "eslint": "^8.24.0", 31 | "eslint-config-prettier": "^8.5.0", 32 | "eslint-plugin-nuxt": "^4.0.0", 33 | "eslint-plugin-vue": "^9.5.1", 34 | "nodemon": "^3.0.3", 35 | "prettier": "3.2.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /web/pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /web/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 51 | -------------------------------------------------------------------------------- /web/plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /web/server/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const consola = require('consola') 3 | const { Nuxt, Builder } = require('nuxt') 4 | 5 | const app = new Koa() 6 | 7 | const koaCors = require('@koa/cors') 8 | const grpcWebMiddleware = require('grpc-web-middleware') 9 | 10 | // Import and Set Nuxt.js options 11 | const config = require('../nuxt.config.js') 12 | config.dev = !(app.env === 'production') 13 | 14 | async function start() { 15 | // Instantiate nuxt.js 16 | const nuxt = new Nuxt(config) 17 | 18 | const { 19 | host = process.env.HOST || '127.0.0.1', 20 | port = process.env.PORT || 3000 21 | } = nuxt.options.server 22 | 23 | // Build in development 24 | if (config.dev) { 25 | const builder = new Builder(nuxt) 26 | await builder.build() 27 | } else { 28 | await nuxt.ready() 29 | } 30 | 31 | app.use(koaCors()) 32 | 33 | app.use(async (ctx, next) => 34 | grpcWebMiddleware('http://localhost:6565', '/grpc')(ctx.req, ctx.res, next) 35 | ) 36 | 37 | app.use(ctx => { 38 | ctx.status = 200 39 | ctx.respond = false // Bypass Koa's built-in response handling 40 | ctx.req.ctx = ctx // This might be useful later on, e.g. in nuxtServerInit or with nuxt-stash 41 | nuxt.render(ctx.req, ctx.res) 42 | }) 43 | 44 | app.listen(port, host) 45 | consola.ready({ 46 | message: `Server listening on http://${host}:${port}`, 47 | badge: true 48 | }) 49 | } 50 | 51 | start() 52 | -------------------------------------------------------------------------------- /web/static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fb64/grpc-fullstack-demo/2ed0a79da2eeb148c923458eb09b54b85ad4ffbe/web/static/favicon.ico -------------------------------------------------------------------------------- /web/store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /web/store/grpc/news_service_grpc_web_pb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview gRPC-Web generated client stub for demo 3 | * @enhanceable 4 | * @public 5 | */ 6 | 7 | // Code generated by protoc-gen-grpc-web. DO NOT EDIT. 8 | // versions: 9 | // protoc-gen-grpc-web v1.5.0 10 | // protoc v4.25.2 11 | // source: news_service.proto 12 | 13 | 14 | /* eslint-disable */ 15 | // @ts-nocheck 16 | 17 | 18 | 19 | const grpc = {}; 20 | grpc.web = require('grpc-web'); 21 | 22 | const proto = {}; 23 | proto.demo = require('./news_service_pb.js'); 24 | 25 | /** 26 | * @param {string} hostname 27 | * @param {?Object} credentials 28 | * @param {?grpc.web.ClientOptions} options 29 | * @constructor 30 | * @struct 31 | * @final 32 | */ 33 | proto.demo.NewsServiceClient = 34 | function(hostname, credentials, options) { 35 | if (!options) options = {}; 36 | options.format = 'text'; 37 | 38 | /** 39 | * @private @const {!grpc.web.GrpcWebClientBase} The client 40 | */ 41 | this.client_ = new grpc.web.GrpcWebClientBase(options); 42 | 43 | /** 44 | * @private @const {string} The hostname 45 | */ 46 | this.hostname_ = hostname.replace(/\/+$/, ''); 47 | 48 | }; 49 | 50 | 51 | /** 52 | * @param {string} hostname 53 | * @param {?Object} credentials 54 | * @param {?grpc.web.ClientOptions} options 55 | * @constructor 56 | * @struct 57 | * @final 58 | */ 59 | proto.demo.NewsServicePromiseClient = 60 | function(hostname, credentials, options) { 61 | if (!options) options = {}; 62 | options.format = 'text'; 63 | 64 | /** 65 | * @private @const {!grpc.web.GrpcWebClientBase} The client 66 | */ 67 | this.client_ = new grpc.web.GrpcWebClientBase(options); 68 | 69 | /** 70 | * @private @const {string} The hostname 71 | */ 72 | this.hostname_ = hostname.replace(/\/+$/, ''); 73 | 74 | }; 75 | 76 | 77 | /** 78 | * @const 79 | * @type {!grpc.web.MethodDescriptor< 80 | * !proto.demo.NewsRequest, 81 | * !proto.demo.NewsResponse>} 82 | */ 83 | const methodDescriptor_NewsService_getNews = new grpc.web.MethodDescriptor( 84 | '/demo.NewsService/getNews', 85 | grpc.web.MethodType.UNARY, 86 | proto.demo.NewsRequest, 87 | proto.demo.NewsResponse, 88 | /** 89 | * @param {!proto.demo.NewsRequest} request 90 | * @return {!Uint8Array} 91 | */ 92 | function(request) { 93 | return request.serializeBinary(); 94 | }, 95 | proto.demo.NewsResponse.deserializeBinary 96 | ); 97 | 98 | 99 | /** 100 | * @param {!proto.demo.NewsRequest} request The 101 | * request proto 102 | * @param {?Object} metadata User defined 103 | * call metadata 104 | * @param {function(?grpc.web.RpcError, ?proto.demo.NewsResponse)} 105 | * callback The callback function(error, response) 106 | * @return {!grpc.web.ClientReadableStream|undefined} 107 | * The XHR Node Readable Stream 108 | */ 109 | proto.demo.NewsServiceClient.prototype.getNews = 110 | function(request, metadata, callback) { 111 | return this.client_.rpcCall(this.hostname_ + 112 | '/demo.NewsService/getNews', 113 | request, 114 | metadata || {}, 115 | methodDescriptor_NewsService_getNews, 116 | callback); 117 | }; 118 | 119 | 120 | /** 121 | * @param {!proto.demo.NewsRequest} request The 122 | * request proto 123 | * @param {?Object=} metadata User defined 124 | * call metadata 125 | * @return {!Promise} 126 | * Promise that resolves to the response 127 | */ 128 | proto.demo.NewsServicePromiseClient.prototype.getNews = 129 | function(request, metadata) { 130 | return this.client_.unaryCall(this.hostname_ + 131 | '/demo.NewsService/getNews', 132 | request, 133 | metadata || {}, 134 | methodDescriptor_NewsService_getNews); 135 | }; 136 | 137 | 138 | /** 139 | * @const 140 | * @type {!grpc.web.MethodDescriptor< 141 | * !proto.demo.SubscribeRequest, 142 | * !proto.demo.News>} 143 | */ 144 | const methodDescriptor_NewsService_subscribe = new grpc.web.MethodDescriptor( 145 | '/demo.NewsService/subscribe', 146 | grpc.web.MethodType.SERVER_STREAMING, 147 | proto.demo.SubscribeRequest, 148 | proto.demo.News, 149 | /** 150 | * @param {!proto.demo.SubscribeRequest} request 151 | * @return {!Uint8Array} 152 | */ 153 | function(request) { 154 | return request.serializeBinary(); 155 | }, 156 | proto.demo.News.deserializeBinary 157 | ); 158 | 159 | 160 | /** 161 | * @param {!proto.demo.SubscribeRequest} request The request proto 162 | * @param {?Object=} metadata User defined 163 | * call metadata 164 | * @return {!grpc.web.ClientReadableStream} 165 | * The XHR Node Readable Stream 166 | */ 167 | proto.demo.NewsServiceClient.prototype.subscribe = 168 | function(request, metadata) { 169 | return this.client_.serverStreaming(this.hostname_ + 170 | '/demo.NewsService/subscribe', 171 | request, 172 | metadata || {}, 173 | methodDescriptor_NewsService_subscribe); 174 | }; 175 | 176 | 177 | /** 178 | * @param {!proto.demo.SubscribeRequest} request The request proto 179 | * @param {?Object=} metadata User defined 180 | * call metadata 181 | * @return {!grpc.web.ClientReadableStream} 182 | * The XHR Node Readable Stream 183 | */ 184 | proto.demo.NewsServicePromiseClient.prototype.subscribe = 185 | function(request, metadata) { 186 | return this.client_.serverStreaming(this.hostname_ + 187 | '/demo.NewsService/subscribe', 188 | request, 189 | metadata || {}, 190 | methodDescriptor_NewsService_subscribe); 191 | }; 192 | 193 | 194 | /** 195 | * @const 196 | * @type {!grpc.web.MethodDescriptor< 197 | * !proto.demo.News, 198 | * !proto.demo.News>} 199 | */ 200 | const methodDescriptor_NewsService_postNews = new grpc.web.MethodDescriptor( 201 | '/demo.NewsService/postNews', 202 | grpc.web.MethodType.UNARY, 203 | proto.demo.News, 204 | proto.demo.News, 205 | /** 206 | * @param {!proto.demo.News} request 207 | * @return {!Uint8Array} 208 | */ 209 | function(request) { 210 | return request.serializeBinary(); 211 | }, 212 | proto.demo.News.deserializeBinary 213 | ); 214 | 215 | 216 | /** 217 | * @param {!proto.demo.News} request The 218 | * request proto 219 | * @param {?Object} metadata User defined 220 | * call metadata 221 | * @param {function(?grpc.web.RpcError, ?proto.demo.News)} 222 | * callback The callback function(error, response) 223 | * @return {!grpc.web.ClientReadableStream|undefined} 224 | * The XHR Node Readable Stream 225 | */ 226 | proto.demo.NewsServiceClient.prototype.postNews = 227 | function(request, metadata, callback) { 228 | return this.client_.rpcCall(this.hostname_ + 229 | '/demo.NewsService/postNews', 230 | request, 231 | metadata || {}, 232 | methodDescriptor_NewsService_postNews, 233 | callback); 234 | }; 235 | 236 | 237 | /** 238 | * @param {!proto.demo.News} request The 239 | * request proto 240 | * @param {?Object=} metadata User defined 241 | * call metadata 242 | * @return {!Promise} 243 | * Promise that resolves to the response 244 | */ 245 | proto.demo.NewsServicePromiseClient.prototype.postNews = 246 | function(request, metadata) { 247 | return this.client_.unaryCall(this.hostname_ + 248 | '/demo.NewsService/postNews', 249 | request, 250 | metadata || {}, 251 | methodDescriptor_NewsService_postNews); 252 | }; 253 | 254 | 255 | module.exports = proto.demo; 256 | 257 | -------------------------------------------------------------------------------- /web/store/news.js: -------------------------------------------------------------------------------- 1 | import { NewsRequest, SubscribeRequest } from './grpc/news_service_pb.js' 2 | import grpc from './grpc/news_service_grpc_web_pb.js' 3 | 4 | const newsService = new grpc.NewsServiceClient('http://localhost:3000/grpc') 5 | let currentSubscription = null 6 | 7 | export const state = () => ({ 8 | newsList: [], 9 | topic: null, 10 | breakingNews: null 11 | }) 12 | 13 | export const mutations = { 14 | loadNews(state, newsList) { 15 | state.newsList = newsList 16 | }, 17 | pushNews(state, news) { 18 | state.newsList.push(news) 19 | state.breakingNews = news 20 | }, 21 | updateTopic(state, topic) { 22 | state.topic = topic 23 | } 24 | } 25 | 26 | export const actions = { 27 | getNews({ commit }, topic) { 28 | const request = new NewsRequest() 29 | request.setTopic(topic) 30 | newsService.getNews(request, {}, function(err, response) { 31 | if (!err) { 32 | commit('updateTopic', topic) 33 | commit('loadNews', response.getNewsList()) 34 | } 35 | }) 36 | }, 37 | subscribe({ commit }, topic) { 38 | const subscribeRequest = new SubscribeRequest() 39 | subscribeRequest.setTopic(topic) 40 | if (currentSubscription !== null) currentSubscription.cancel() 41 | currentSubscription = newsService.subscribe(subscribeRequest, {}) 42 | currentSubscription.on('data', function(response) { 43 | commit('pushNews', response) 44 | }) 45 | } 46 | } 47 | --------------------------------------------------------------------------------