├── .gitattributes ├── .github └── workflows │ └── android_conferences.yml ├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml └── misc.xml ├── README.md ├── app-debug.apk ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── in │ │ └── jitinsharma │ │ └── android │ │ └── conf │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── in │ │ │ └── jitinsharma │ │ │ └── android │ │ │ └── conf │ │ │ ├── ConferenceApplication.kt │ │ │ ├── database │ │ │ ├── AppDatabase.kt │ │ │ └── ConferenceDataDao.kt │ │ │ ├── di │ │ │ └── ConferenceModule.kt │ │ │ ├── model │ │ │ ├── ConferenceData.kt │ │ │ └── Country.kt │ │ │ ├── network │ │ │ └── ConferenceApi.kt │ │ │ ├── notification │ │ │ └── NotificationHelper.kt │ │ │ ├── preferences │ │ │ └── AppPreferences.kt │ │ │ ├── repository │ │ │ └── ConferenceRepositoryImpl.kt │ │ │ ├── sync │ │ │ └── ConferenceUpdateWorker.kt │ │ │ ├── ui │ │ │ ├── ConferenceApp.kt │ │ │ ├── ConferenceCard.kt │ │ │ ├── ConferenceCardList.kt │ │ │ ├── ConferencePage.kt │ │ │ ├── FiltersScreen.kt │ │ │ ├── Header.kt │ │ │ ├── LoadingView.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SettingsPage.kt │ │ │ ├── Themes.kt │ │ │ └── WtfView.kt │ │ │ ├── utils │ │ │ ├── DispatcherProvider.kt │ │ │ └── PreviewUtils.kt │ │ │ └── viewmodel │ │ │ ├── ConferenceViewModel.kt │ │ │ └── FilterScreenViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_android_symbol_green.xml │ │ ├── ic_baseline_alarm_add.xml │ │ ├── ic_baseline_calendar_today.xml │ │ ├── ic_baseline_filter_list.xml │ │ ├── ic_baseline_location_on.xml │ │ ├── ic_baseline_settings.xml │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── styles.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── in │ └── jitinsharma │ └── android │ └── conf │ ├── ConferenceViewModelTest.kt │ └── CoroutineTestRule.kt ├── art ├── screenshot.png └── screenshot_filter.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/android_conferences.yml: -------------------------------------------------------------------------------- 1 | name: Android Conferences 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 11 22 | 23 | - name: Speed-up by Restoring Gradle Cache from Previous Builds 24 | uses: actions/cache@v2 25 | with: 26 | path: | 27 | ~/.gradle/caches 28 | ~/.gradle/wrapper 29 | key: ${{runner.os}}-gradle-${{hashFiles('**/*.gradle*')}} 30 | restore-keys: | 31 | ${{runner.os}}-gradle- 32 | 33 | - name: Build project 34 | run: ./gradlew assembleDebug 35 | 36 | - name: Uploading APK 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: app 40 | path: app/build/outputs/apk/debug/**.apk 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.ap_ 3 | *.aab 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/* 38 | # Keystore files 39 | # Uncomment the following lines if you do not want to check your keystore files in. 40 | #*.jks 41 | #*.keystore 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Google Services (e.g. APIs or Firebase) 47 | google-services.json 48 | 49 | # Freeline 50 | freeline.py 51 | freeline/ 52 | freeline_project_description.json 53 | 54 | # fastlane 55 | fastlane/report.xml 56 | fastlane/Preview.html 57 | fastlane/screenshots 58 | fastlane/test_output 59 | fastlane/readme.md 60 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | android-conferences -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 50 | 51 | 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 | 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # android-conferences 2 | 3 | An app which displays list of conferences happening all over the world sourced from 4 | [this](http://androidstudygroup.github.io/conferences/) website. 5 | 6 | ### Install 7 | Get latest apk from [Actions run](https://github.com/jitinsharma/android-conferences/actions/) 8 | 9 | or build from source by running `./gradlew assembleDebug` 10 | 11 | ### Features 12 | - Display conferences with relevant details. ✅ 13 | - Offline support. ✅ 14 | - Filter conferences by cfp status and by country. ✅ 15 | - CFP reminder. 16 | - New conference reminder. ✅ 17 | 18 | ### Objectives for this project 19 | This project is being built with Jetpack Compose with following objectives in mind 20 | - Build a multi screen UI with Compose. 21 | - Understand how state management will work with Compose 22 | - Make app reactive to changes from network + DB as well as implement a simple UI filter 23 | - Make it work with Flow/coroutines 24 | - Keep it updated with releases of Compose 25 | 26 | ## Screenshots 27 | ![](/art/screenshot.png) 28 | ![](/art/screenshot_filter.png) 29 | -------------------------------------------------------------------------------- /app-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app-debug.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | android { 8 | compileSdkVersion 31 9 | 10 | defaultConfig { 11 | applicationId "in.jitinsharma.asg.conf" 12 | minSdkVersion 21 13 | targetSdkVersion 31 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | 18 | javaCompileOptions { 19 | annotationProcessorOptions { 20 | arguments = [ 21 | "room.incremental" : "true", 22 | "room.expandProjection": "true"] 23 | } 24 | } 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | buildFeatures { 38 | compose true 39 | } 40 | composeOptions { 41 | kotlinCompilerExtensionVersion "$compose_version" 42 | } 43 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { 44 | kotlinOptions { 45 | freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' 46 | // Enable experimental coroutines APIs, including Flow 47 | freeCompilerArgs += '-Xopt-in=kotlin.Experimental' 48 | 49 | // Set JVM target to 1.8 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | } 54 | 55 | dependencies { 56 | implementation 'androidx.core:core-ktx:1.8.0' 57 | implementation 'androidx.appcompat:appcompat:1.4.2' 58 | implementation 'com.google.android.material:material:1.6.1' 59 | implementation "androidx.compose.runtime:runtime:$compose_version" 60 | implementation "androidx.compose.ui:ui:$compose_version" 61 | implementation "androidx.compose.foundation:foundation-layout:$compose_version" 62 | implementation "androidx.compose.material:material:$compose_version" 63 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 64 | implementation "androidx.compose.runtime:runtime-livedata:$compose_version" 65 | implementation "androidx.compose.foundation:foundation:$compose_version" 66 | testImplementation 'junit:junit:4.13.2' 67 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3' 68 | testImplementation "org.mockito:mockito-core:3.10.0" 69 | testImplementation 'org.mockito:mockito-inline:3.10.0' 70 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 71 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 72 | implementation 'org.jsoup:jsoup:1.13.1' 73 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0" 74 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0" 75 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1" 76 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" 77 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1" 78 | implementation "androidx.activity:activity-ktx:1.4.0" 79 | 80 | def room_version = "2.4.2" 81 | 82 | implementation "androidx.room:room-runtime:$room_version" 83 | implementation "androidx.room:room-ktx:$room_version" 84 | kapt "androidx.room:room-compiler:$room_version" 85 | 86 | // Koin main features for Android 87 | implementation "io.insert-koin:koin-android:$koin_version" 88 | implementation "io.insert-koin:koin-androidx-workmanager:$koin_version" 89 | implementation "io.insert-koin:koin-androidx-navigation:$koin_version" 90 | 91 | implementation "androidx.datastore:datastore-preferences:1.0.0" 92 | 93 | implementation "androidx.work:work-runtime-ktx:2.7.1" 94 | 95 | def nav_compose_version = "2.5.0-rc01" 96 | implementation "androidx.navigation:navigation-compose:$nav_compose_version" 97 | } 98 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/androidTest/java/in/jitinsharma/android/conf/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("in.jitinsharma.asg.conf", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ConferenceApplication.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf 2 | 3 | import `in`.jitinsharma.android.conf.di.conferenceModule 4 | import `in`.jitinsharma.android.conf.sync.ConferenceUpdateWorker 5 | import android.app.Application 6 | import androidx.work.WorkManager 7 | import org.koin.android.ext.koin.androidContext 8 | import org.koin.core.context.startKoin 9 | 10 | class ConferenceApplication : Application() { 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | startKoin { 15 | androidContext(this@ConferenceApplication) 16 | modules(conferenceModule) 17 | } 18 | WorkManager.getInstance(this).enqueue(ConferenceUpdateWorker.getPeriodicRequest()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.database 2 | 3 | import `in`.jitinsharma.android.conf.model.ConferenceData 4 | import android.content.Context 5 | import androidx.room.Database 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | 9 | @Database(entities = [ConferenceData::class], version = 1, exportSchema = false) 10 | abstract class AppDatabase : RoomDatabase() { 11 | 12 | abstract fun conferenceDataDao(): ConferenceDataDao 13 | 14 | companion object { 15 | 16 | fun getDatabase(applicationContext: Context): AppDatabase { 17 | return Room.databaseBuilder( 18 | applicationContext, 19 | AppDatabase::class.java, "androidconfdb" 20 | ).build() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/database/ConferenceDataDao.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.database 2 | 3 | import `in`.jitinsharma.android.conf.model.ConferenceData 4 | import androidx.room.* 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | @Dao 8 | interface ConferenceDataDao { 9 | 10 | @Query("SELECT * FROM conferenceData") 11 | fun getConferenceDataList(): Flow> 12 | 13 | @Insert(onConflict = OnConflictStrategy.REPLACE) 14 | suspend fun storeConferenceData(vararg conferenceData: ConferenceData) 15 | 16 | @Query("DELETE FROM conferenceData") 17 | suspend fun deleteAllConferenceData() 18 | 19 | @Transaction 20 | suspend fun replaceAndStoreConferenceData(vararg conferenceData: ConferenceData) { 21 | deleteAllConferenceData() 22 | storeConferenceData(conferenceData = conferenceData) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/di/ConferenceModule.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.di 2 | 3 | import `in`.jitinsharma.android.conf.database.AppDatabase 4 | import `in`.jitinsharma.android.conf.preferences.AppPreferences 5 | import `in`.jitinsharma.android.conf.repository.ConferenceRepository 6 | import `in`.jitinsharma.android.conf.repository.ConferenceRepositoryImpl 7 | import `in`.jitinsharma.android.conf.viewmodel.ConferenceViewModel 8 | import `in`.jitinsharma.android.conf.viewmodel.FilterScreenViewModel 9 | import org.koin.android.ext.koin.androidApplication 10 | import org.koin.androidx.viewmodel.dsl.viewModel 11 | import org.koin.dsl.module 12 | 13 | val conferenceModule = module { 14 | single { AppDatabase.getDatabase(applicationContext = androidApplication()) } 15 | factory { ConferenceRepositoryImpl(appDatabase = get()) } 16 | single { AppPreferences(context = androidApplication()) } 17 | viewModel { ConferenceViewModel(get()) } 18 | viewModel { FilterScreenViewModel(get()) } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/model/ConferenceData.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.model 2 | 3 | import androidx.annotation.NonNull 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Embedded 6 | import androidx.room.Entity 7 | import androidx.room.PrimaryKey 8 | 9 | @Entity 10 | data class ConferenceData( 11 | @ColumnInfo var name: String = "", 12 | @ColumnInfo var city: String = "", 13 | @PrimaryKey @NonNull var id: String = "$name$city", 14 | @ColumnInfo var country: String = "", 15 | @ColumnInfo var url: String = "https://www.droidcon.com/", 16 | @ColumnInfo var date: String = "", 17 | @ColumnInfo var status: String = "Active", 18 | @ColumnInfo var isPast: Boolean = false, 19 | @ColumnInfo var isActive: Boolean = true, 20 | @Embedded var cfpData: CfpData? = null 21 | ) { 22 | data class CfpData( 23 | @ColumnInfo var cfpDate: String = "", 24 | @ColumnInfo var cfpUrl: String = "", 25 | @ColumnInfo var isCfpActive: Boolean = true 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/model/Country.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.model 2 | 3 | data class Country( 4 | var name: String 5 | ) 6 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/network/ConferenceApi.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.network 2 | 3 | import kotlinx.coroutines.coroutineScope 4 | import org.jsoup.Jsoup 5 | import org.jsoup.nodes.Document 6 | 7 | private const val baseUrl = "https://androidstudygroup.github.io/conferences/" 8 | 9 | @Suppress("BlockingMethodInNonBlockingContext") 10 | suspend fun getHTMLData(): Document = 11 | coroutineScope { 12 | Jsoup.connect(baseUrl).get() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/notification/NotificationHelper.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.notification 2 | 3 | import `in`.jitinsharma.android.conf.R 4 | import `in`.jitinsharma.android.conf.model.ConferenceData 5 | import `in`.jitinsharma.android.conf.ui.MainActivity 6 | import android.app.NotificationChannel 7 | import android.app.NotificationManager 8 | import android.app.PendingIntent 9 | import android.content.Context 10 | import android.content.Intent 11 | import android.os.Build 12 | import androidx.core.app.NotificationCompat 13 | import androidx.core.app.NotificationManagerCompat 14 | 15 | object NotificationHelper { 16 | 17 | private const val CONF_UPDATE_CHANNEL = "CONFERENCE_UPDATE" 18 | 19 | fun sendConferenceUpdateNotification(context: Context, conferenceList: List) { 20 | val intent = Intent(context, MainActivity::class.java).apply { 21 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 22 | } 23 | val pendingIntent: PendingIntent = 24 | PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, 0) 25 | createNotificationChannel(context) 26 | 27 | val builder = NotificationCompat.Builder(context, CONF_UPDATE_CHANNEL) 28 | .setSmallIcon(R.drawable.ic_launcher_foreground) 29 | .setContentTitle("Conferences Updated/Added") 30 | .setStyle( 31 | NotificationCompat.BigTextStyle().bigText(getConferenceListText(conferenceList)) 32 | ) 33 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 34 | .setContentIntent(pendingIntent) 35 | .setAutoCancel(true) 36 | NotificationManagerCompat.from(context).notify(1, builder.build()) 37 | } 38 | 39 | private fun createNotificationChannel(context: Context) { 40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 41 | val name = "Conference Updates" 42 | val descriptionText = "Notifications for updates to conferences" 43 | val importance = NotificationManager.IMPORTANCE_DEFAULT 44 | val channel = NotificationChannel(CONF_UPDATE_CHANNEL, name, importance).apply { 45 | description = descriptionText 46 | } 47 | NotificationManagerCompat.from(context).createNotificationChannel(channel) 48 | } 49 | } 50 | 51 | private fun getConferenceListText(conferenceList: List): String { 52 | return buildString { 53 | conferenceList.forEach { 54 | append("- ${it.name}, ${it.country}") 55 | appendLine() 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/preferences/AppPreferences.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.preferences 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.distinctUntilChanged 11 | import kotlinx.coroutines.flow.map 12 | 13 | const val PREF_FILE = "app.preferences_pb" 14 | val UPDATE_PREF_KEY = booleanPreferencesKey("UPDATE_PREF_KEY") 15 | 16 | private val Context.appPreferencesStore: DataStore by preferencesDataStore(name = PREF_FILE) 17 | 18 | class AppPreferences(private val context: Context) { 19 | 20 | suspend fun setUpdatePreference(enabled: Boolean) { 21 | context.appPreferencesStore.edit { preferences -> 22 | preferences[UPDATE_PREF_KEY] = enabled 23 | } 24 | } 25 | 26 | fun getUpdatePreference(): Flow { 27 | return context.appPreferencesStore.data 28 | .map { preferences -> preferences[UPDATE_PREF_KEY] ?: false } 29 | .distinctUntilChanged() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/repository/ConferenceRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.repository 2 | 3 | import `in`.jitinsharma.android.conf.database.AppDatabase 4 | import `in`.jitinsharma.android.conf.model.ConferenceData 5 | import `in`.jitinsharma.android.conf.network.getHTMLData 6 | import android.text.Html 7 | import androidx.core.text.parseAsHtml 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.distinctUntilChanged 10 | import org.jsoup.nodes.Node 11 | import org.jsoup.nodes.TextNode 12 | import java.text.SimpleDateFormat 13 | import java.util.* 14 | import java.util.regex.Pattern 15 | 16 | class ConferenceRepositoryImpl( 17 | private val appDatabase: AppDatabase 18 | ) : ConferenceRepository { 19 | 20 | override suspend fun loadConferenceData() { 21 | val conferenceDataList = getConferenceDataFromNetwork() 22 | if (conferenceDataList.isNotEmpty()) { 23 | addConferenceDataToDB(conferenceDataList) 24 | } 25 | } 26 | 27 | override suspend fun getConferenceDataFromNetwork(): List { 28 | try { 29 | val document = getHTMLData() 30 | val conferenceListElement = 31 | document.getElementsByClass("conference-list list-unstyled")[0] 32 | return conferenceListElement 33 | .childNodes() 34 | .filterNot { conferenceElement -> 35 | (conferenceElement is TextNode) && conferenceElement.isBlank 36 | }.mapTo(ArrayList()) { 37 | it.mapToConferenceDataModel() 38 | }.filterNot { conferenceData -> 39 | conferenceData.isPast 40 | } 41 | } catch (e: Exception) { 42 | throw e 43 | } 44 | } 45 | 46 | override suspend fun addConferenceDataToDB(conferenceDataList: List) { 47 | appDatabase.conferenceDataDao() 48 | .replaceAndStoreConferenceData(*conferenceDataList.toTypedArray()) 49 | } 50 | 51 | override fun getConferenceDataList(): Flow> = 52 | appDatabase.conferenceDataDao().getConferenceDataList().distinctUntilChanged() 53 | 54 | private fun Node.mapToConferenceDataModel(): ConferenceData { 55 | val conferenceDataModel = ConferenceData() 56 | conferenceDataModel.date = childNode(0).toString().replace("  ", "").trim() 57 | conferenceDataModel.isPast = isPastDate(conferenceDataModel.date) 58 | conferenceDataModel.name = childNode(1).childNode(0).toString().trim() 59 | conferenceDataModel.url = childNode(1).attr("href") 60 | val location = childNode(3).childNode(0).toString() 61 | val locationDataArray = location.split(",") 62 | conferenceDataModel.country = locationDataArray.last().trim().parseAsHtml().toString() 63 | conferenceDataModel.city = locationDataArray.dropLast(1).run { 64 | if (size > 1) { 65 | joinToString(",") 66 | } else { 67 | joinToString() 68 | } 69 | }.trim() 70 | conferenceDataModel.id = conferenceDataModel.name + conferenceDataModel.city 71 | if (childNodeSize() > 5) { 72 | conferenceDataModel.parseCfpAndStatusData(childNode(5).toString()) 73 | } 74 | return conferenceDataModel 75 | } 76 | 77 | private fun ConferenceData.parseCfpAndStatusData(data: String) { 78 | data.lines().forEach { line -> 79 | when { 80 | line.contains("Call For Papers") -> { 81 | val cfpData = ConferenceData.CfpData() 82 | //TODO Try replacing matchers by parsing line with Jsoup 83 | val cfpUrlMatcher = 84 | Pattern.compile("href=(.*?)>", Pattern.DOTALL).matcher(line) 85 | val cfpDateMatcher = Pattern.compile(">(.*?)<", Pattern.DOTALL).matcher(line) 86 | while (cfpUrlMatcher.find()) { 87 | val match = cfpUrlMatcher.group(1) 88 | if (!match.isNullOrBlank()) { 89 | cfpData.cfpUrl = match.replace("\"", "") 90 | } 91 | } 92 | while (cfpDateMatcher.find()) { 93 | val match = cfpDateMatcher.group(1) 94 | if (!match.isNullOrBlank()) { 95 | cfpData.cfpDate = match.replace("[^\\d-]".toRegex(), "") 96 | cfpData.isCfpActive = isPastDate(cfpData.cfpDate).not() 97 | } 98 | } 99 | this.cfpData = cfpData 100 | } 101 | line.contains("label-danger") && !line.contains("online", ignoreCase = true) -> { 102 | this.isActive = false 103 | } 104 | } 105 | } 106 | } 107 | 108 | private fun isPastDate(date: String): Boolean { 109 | val currentDate = Date(System.currentTimeMillis()) 110 | val conferenceDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date) 111 | return requireNotNull(conferenceDate) < currentDate 112 | } 113 | } 114 | 115 | interface ConferenceRepository { 116 | suspend fun loadConferenceData() 117 | suspend fun getConferenceDataFromNetwork(): List 118 | suspend fun addConferenceDataToDB(conferenceDataList: List) 119 | fun getConferenceDataList(): Flow> 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/sync/ConferenceUpdateWorker.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.sync 2 | 3 | import `in`.jitinsharma.android.conf.notification.NotificationHelper 4 | import `in`.jitinsharma.android.conf.preferences.AppPreferences 5 | import `in`.jitinsharma.android.conf.repository.ConferenceRepositoryImpl 6 | import `in`.jitinsharma.android.conf.utils.AppDispatchers 7 | import android.content.Context 8 | import androidx.work.CoroutineWorker 9 | import androidx.work.OneTimeWorkRequestBuilder 10 | import androidx.work.PeriodicWorkRequestBuilder 11 | import androidx.work.WorkerParameters 12 | import kotlinx.coroutines.flow.* 13 | import kotlinx.coroutines.withContext 14 | import org.koin.core.component.KoinComponent 15 | import org.koin.core.component.inject 16 | import java.util.concurrent.TimeUnit 17 | 18 | class ConferenceUpdateWorker(appContext: Context, params: WorkerParameters) : 19 | CoroutineWorker(appContext, params), KoinComponent { 20 | 21 | private val repository: ConferenceRepositoryImpl by inject() 22 | private val appPreferences: AppPreferences by inject() 23 | private val dispatcherProvider = AppDispatchers 24 | 25 | override suspend fun doWork(): Result { 26 | return withContext(dispatcherProvider.io) { 27 | val conferenceList = repository.getConferenceDataFromNetwork() 28 | val storedConferenceList = repository.getConferenceDataList().first() 29 | val diff = conferenceList.filterNot { storedConferenceList.contains(it) } 30 | if (diff.isNotEmpty()) { 31 | appPreferences 32 | .getUpdatePreference() 33 | .collect { updatePreference -> 34 | if (updatePreference) { 35 | NotificationHelper.sendConferenceUpdateNotification( 36 | applicationContext, 37 | diff 38 | ) 39 | } 40 | } 41 | } 42 | repository.addConferenceDataToDB(conferenceList) 43 | return@withContext Result.success() 44 | } 45 | } 46 | 47 | companion object { 48 | fun getPeriodicRequest() = 49 | PeriodicWorkRequestBuilder(1, TimeUnit.DAYS).build() 50 | 51 | // For testing 52 | fun getImmediateRequest() = 53 | OneTimeWorkRequestBuilder().build() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/ConferenceApp.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.viewmodel.ConferenceViewModel 4 | import `in`.jitinsharma.android.conf.viewmodel.FilterScreenViewModel 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.collectAsState 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.navigation.NavController 15 | import androidx.navigation.compose.* 16 | import kotlinx.coroutines.ExperimentalCoroutinesApi 17 | 18 | @ExperimentalCoroutinesApi 19 | @Composable 20 | fun ConferenceApp( 21 | conferenceViewModel: ConferenceViewModel, 22 | filterScreenViewModel: FilterScreenViewModel 23 | ) { 24 | Box( 25 | modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colors.primary) 26 | ) { 27 | Column(modifier = Modifier.fillMaxWidth().padding(all = 16.dp)) { 28 | val filterScreenDialogState = remember { mutableStateOf(false) } 29 | val navController = rememberNavController() 30 | val conferenceUiState = conferenceViewModel.uiState.collectAsState() 31 | 32 | Header( 33 | modifier = Modifier.padding(bottom = 16.dp), 34 | onFilterClicked = { 35 | filterScreenDialogState.value = filterScreenDialogState.value.not() 36 | }, 37 | onAndroidIconClicked = { 38 | if (navController.currentRoute != ConferenceListScreen) { 39 | navController.navigate(ConferenceListScreen) 40 | } 41 | }, 42 | onSettingsClicked = { 43 | if (navController.currentRoute != SettingsScreen) { 44 | navController.navigate(SettingsScreen) 45 | } 46 | } 47 | ) 48 | 49 | NavHost(navController = navController, startDestination = ConferenceListScreen) { 50 | composable(ConferenceListScreen) { 51 | ConferencePage( 52 | conferenceListUiState = conferenceUiState.value, 53 | onRetryClick = { 54 | conferenceViewModel.loadConferences() 55 | } 56 | ) 57 | } 58 | composable(SettingsScreen) { SettingsPage() } 59 | } 60 | 61 | if (filterScreenDialogState.value) { 62 | val filterScreenUiState = filterScreenViewModel.uiState.collectAsState() 63 | FilterDialog( 64 | filterScreenUiState = filterScreenUiState.value, 65 | onDismissRequest = { 66 | filterScreenDialogState.value = false 67 | }, 68 | onFilterRequest = { cfpFilterChecked, selectedCountries -> 69 | conferenceViewModel.filterConferences(cfpFilterChecked, selectedCountries) 70 | filterScreenViewModel.updateUiState(cfpFilterChecked, selectedCountries) 71 | filterScreenDialogState.value = false 72 | } 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | 79 | private val NavController.currentRoute get() = currentDestination?.route 80 | 81 | const val ConferenceListScreen = "ConferenceListScreen" 82 | const val SettingsScreen = "SettingsScreen" -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/ConferenceCard.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.R 4 | import `in`.jitinsharma.android.conf.model.ConferenceData 5 | import `in`.jitinsharma.android.conf.utils.ThemedPreview 6 | import android.content.Context 7 | import android.content.Intent 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material.Card 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.MaterialTheme 14 | import androidx.compose.material.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.res.painterResource 21 | import androidx.compose.ui.text.SpanStyle 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.text.buildAnnotatedString 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.text.style.TextDecoration 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.sp 29 | import androidx.core.net.toUri 30 | import java.text.SimpleDateFormat 31 | import java.util.* 32 | 33 | @Composable 34 | fun ConferenceCard( 35 | modifier: Modifier = Modifier, 36 | conferenceData: ConferenceData 37 | ) { 38 | Card( 39 | shape = RoundedCornerShape(8.dp), 40 | backgroundColor = if (conferenceData.isActive) { 41 | MaterialTheme.colors.secondary 42 | } else { 43 | Color(0x4D3DDB85) 44 | }, 45 | contentColor = MaterialTheme.colors.primary, 46 | modifier = modifier.wrapContentHeight(align = Alignment.CenterVertically) 47 | ) { 48 | val context = LocalContext.current 49 | Column(modifier = Modifier.padding(all = 16.dp)) { 50 | Row( 51 | modifier = Modifier.fillMaxWidth(), 52 | ) { 53 | Box( 54 | Modifier 55 | .clickable( 56 | onClick = { context.loadUrl(conferenceData.url) } 57 | ) 58 | .weight(7f) 59 | ) { 60 | Text( 61 | text = conferenceData.name, 62 | style = TextStyle( 63 | textDecoration = TextDecoration.Underline, 64 | fontWeight = FontWeight.Bold, 65 | fontSize = 18.sp 66 | ) 67 | ) 68 | } 69 | 70 | Row(modifier = Modifier.weight(3f)) { 71 | Icon( 72 | modifier = Modifier.padding(end = 4.dp), 73 | painter = painterResource(id = R.drawable.ic_baseline_calendar_today), 74 | contentDescription = null 75 | ) 76 | Text( 77 | text = conferenceData.date, 78 | style = TextStyle( 79 | fontWeight = FontWeight.Bold, 80 | fontSize = 12.sp 81 | ) 82 | ) 83 | } 84 | } 85 | 86 | Row { 87 | Icon( 88 | modifier = Modifier.padding(top = 4.dp, end = 4.dp), 89 | painter = painterResource( 90 | id = R.drawable.ic_baseline_location_on 91 | ), 92 | contentDescription = null 93 | ) 94 | 95 | val location = if (conferenceData.city.isNotBlank()) { 96 | "${conferenceData.city}, ${conferenceData.country}" 97 | } else { 98 | conferenceData.country 99 | } 100 | 101 | Text( 102 | text = location, 103 | style = TextStyle(fontSize = 12.sp), 104 | modifier = Modifier.padding(top = 2.dp) 105 | ) 106 | } 107 | 108 | if (showCfp(conferenceData.cfpData)) { 109 | val cfpData = conferenceData.cfpData!! 110 | Box(Modifier.clickable( 111 | onClick = { context.loadUrl(cfpData.cfpUrl) } 112 | )) { 113 | Text( 114 | text = buildAnnotatedString { 115 | append("CFP closes on ") 116 | pushStyle( 117 | SpanStyle( 118 | textDecoration = TextDecoration.Underline, 119 | fontWeight = FontWeight.Bold 120 | ) 121 | ) 122 | append(cfpData.cfpDate) 123 | }, 124 | style = TextStyle(fontSize = 12.sp), 125 | modifier = Modifier.padding(top = 16.dp) 126 | ) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | 134 | private fun showCfp(cfpData: ConferenceData.CfpData?): Boolean { 135 | cfpData?.let { 136 | val currentDate = Date(System.currentTimeMillis()) 137 | val cfpDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(cfpData.cfpDate) 138 | return requireNotNull(cfpDate) > currentDate 139 | } ?: return false 140 | } 141 | 142 | private fun Context.loadUrl(url: String) { 143 | startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) 144 | } 145 | 146 | @Preview 147 | @Composable 148 | fun ConferenceCardPreview() { 149 | ThemedPreview { 150 | ConferenceCard( 151 | conferenceData = ConferenceData( 152 | name = "Droidcon", 153 | city = "New York City, NY", 154 | country = "USA", 155 | date = "2020-08-31", 156 | isActive = true, 157 | cfpData = ConferenceData.CfpData( 158 | cfpDate = "2020-06-30" 159 | ) 160 | ) 161 | ) 162 | } 163 | } 164 | 165 | @Preview 166 | @Composable 167 | fun MultiLineConferenceCardPreview() { 168 | ThemedPreview { 169 | ConferenceCard( 170 | conferenceData = ConferenceData( 171 | name = "DevCommunity Summit(Previously Android Summit)", 172 | city = "Online", 173 | date = "2020-10-01", 174 | isActive = true, 175 | ) 176 | ) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/ConferenceCardList.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.model.ConferenceData 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.itemsIndexed 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Composable 12 | fun ConferenceCardList( 13 | conferenceDataList: List 14 | ) { 15 | LazyColumn { 16 | itemsIndexed(items = conferenceDataList, itemContent = { index, item -> 17 | ConferenceCard( 18 | modifier = if (index == 0) { 19 | Modifier.padding(top = 16.dp) 20 | } else { 21 | Modifier.padding(top = 32.dp) 22 | }, 23 | conferenceData = item 24 | ) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/ConferencePage.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.viewmodel.ConferenceListUiState 4 | import androidx.compose.animation.Crossfade 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | fun ConferencePage( 9 | conferenceListUiState: ConferenceListUiState, 10 | onRetryClick: () -> Unit 11 | ) { 12 | Crossfade(targetState = conferenceListUiState) { uiState -> 13 | when (uiState) { 14 | is ConferenceListUiState.Success -> { 15 | ConferenceCardList(conferenceDataList = uiState.conferenceList) 16 | } 17 | is ConferenceListUiState.Loading -> { 18 | LoadingView() 19 | } 20 | is ConferenceListUiState.Error -> { 21 | WtfView(onRetryClick = onRetryClick) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/FiltersScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.model.Country 4 | import `in`.jitinsharma.android.conf.utils.ThemedPreview 5 | import `in`.jitinsharma.android.conf.viewmodel.FilterScreenUiState 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.material.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.window.Dialog 22 | 23 | @Composable 24 | fun FilterDialog( 25 | filterScreenUiState: FilterScreenUiState, 26 | onDismissRequest: () -> Unit, 27 | onFilterRequest: (cfpFilterChecked: Boolean, selectedCountries: List) -> Unit 28 | ) { 29 | Dialog(onDismissRequest = { onDismissRequest() }) { 30 | when (filterScreenUiState) { 31 | is FilterScreenUiState.Success -> { 32 | FiltersScreen( 33 | cfpFilterChecked = filterScreenUiState.cfpFilterChecked, 34 | selectedCountries = filterScreenUiState.selectedCountries.toMutableList(), 35 | countyList = filterScreenUiState.countryList, 36 | onDismiss = onDismissRequest, 37 | onApply = onFilterRequest 38 | ) 39 | } 40 | } 41 | } 42 | } 43 | 44 | @Composable 45 | fun FiltersScreen( 46 | cfpFilterChecked: Boolean = false, 47 | selectedCountries: MutableList, 48 | countyList: List?, 49 | onDismiss: () -> Unit, 50 | onApply: (cfpFilterChecked: Boolean, selectedCountries: List) -> Unit 51 | ) { 52 | Card(backgroundColor = themeColors.secondary) { 53 | Column(modifier = Modifier.wrapContentSize()) { 54 | Box( 55 | modifier = Modifier 56 | .background(color = themeColors.primary) 57 | .fillMaxWidth() 58 | .align(Alignment.CenterHorizontally) 59 | .padding(8.dp) 60 | ) { 61 | Text( 62 | text = "FILTERS", 63 | color = themeColors.secondary, 64 | style = MaterialTheme.typography.h6 65 | ) 66 | } 67 | 68 | Spacer(modifier = Modifier.height(4.dp)) 69 | 70 | Text( 71 | text = "Cfp Status", 72 | modifier = Modifier.padding(start = 8.dp), 73 | color = themeColors.primary, 74 | style = MaterialTheme.typography.body1.merge( 75 | other = TextStyle( 76 | fontWeight = FontWeight.Bold 77 | ) 78 | ) 79 | ) 80 | 81 | Spacer(modifier = Modifier.height(4.dp)) 82 | 83 | val cfpFilterCheckState = remember { mutableStateOf(cfpFilterChecked) } 84 | Box( 85 | Modifier 86 | .padding(start = 8.dp) 87 | .clickable( 88 | onClick = { 89 | cfpFilterCheckState.value = cfpFilterCheckState.value.not() 90 | }, enabled = true 91 | ) 92 | ) { 93 | Row( 94 | modifier = Modifier.fillMaxWidth(), 95 | verticalAlignment = Alignment.CenterVertically 96 | ) { 97 | Checkbox( 98 | checked = cfpFilterCheckState.value, 99 | onCheckedChange = { 100 | cfpFilterCheckState.value = cfpFilterCheckState.value.not() 101 | }, 102 | colors = CheckboxDefaults.colors( 103 | checkedColor = MaterialTheme.colors.primary, 104 | uncheckedColor = MaterialTheme.colors.primary 105 | ) 106 | ) 107 | Text( 108 | text = "Cfp Open", 109 | color = themeColors.primary, 110 | style = MaterialTheme.typography.body2 111 | ) 112 | } 113 | } 114 | 115 | Spacer(modifier = Modifier.height(8.dp)) 116 | 117 | countyList?.let { 118 | CountryList( 119 | countyList = it, 120 | selectedCountries = selectedCountries 121 | ) 122 | } 123 | 124 | Box( 125 | modifier = Modifier 126 | .background(color = themeColors.primary) 127 | .padding(8.dp) 128 | ) { 129 | Row( 130 | modifier = Modifier.fillMaxWidth(), 131 | horizontalArrangement = Arrangement.SpaceBetween 132 | ) { 133 | Box( 134 | Modifier.clickable( 135 | onClick = { onDismiss() }) 136 | ) { 137 | Text( 138 | text = "CANCEL", 139 | modifier = Modifier.padding(start = 8.dp), 140 | color = themeColors.secondary, 141 | style = MaterialTheme.typography.button.merge( 142 | other = TextStyle( 143 | fontWeight = FontWeight.Bold 144 | ) 145 | ) 146 | ) 147 | } 148 | Box( 149 | Modifier.clickable( 150 | onClick = { onApply(cfpFilterCheckState.value, selectedCountries) }) 151 | ) { 152 | Text( 153 | text = "APPLY", 154 | modifier = Modifier.padding(start = 8.dp), 155 | color = themeColors.secondary, 156 | style = MaterialTheme.typography.button.merge( 157 | other = TextStyle( 158 | fontWeight = FontWeight.Bold 159 | ) 160 | ) 161 | ) 162 | } 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | @Composable 170 | fun CountryList( 171 | countyList: List, 172 | selectedCountries: MutableList 173 | ) { 174 | Column( 175 | modifier = Modifier.height(360.dp) 176 | ) { 177 | Text( 178 | text = "Countries", 179 | color = themeColors.primary, 180 | modifier = Modifier.padding(start = 8.dp), 181 | style = MaterialTheme.typography.body1.merge( 182 | other = TextStyle( 183 | fontWeight = FontWeight.Bold 184 | ) 185 | ) 186 | ) 187 | Spacer(modifier = Modifier.height(8.dp)) 188 | LazyColumn(modifier = Modifier.wrapContentHeight(align = Alignment.CenterVertically)) { 189 | items(items = countyList, 190 | itemContent = { country -> 191 | val countryChecked = 192 | remember { mutableStateOf(selectedCountries.contains(country)) } 193 | Box(Modifier.clickable( 194 | onClick = { 195 | countryChecked.value = countryChecked.value.not() 196 | if (countryChecked.value) { 197 | selectedCountries.add(country) 198 | } else { 199 | selectedCountries.remove(country) 200 | } 201 | } 202 | )) { 203 | Row( 204 | modifier = Modifier 205 | .fillMaxWidth() 206 | .padding(start = 8.dp, bottom = 4.dp), 207 | verticalAlignment = Alignment.CenterVertically, 208 | ) { 209 | Checkbox( 210 | checked = countryChecked.value, 211 | onCheckedChange = { 212 | countryChecked.value = countryChecked.value.not() 213 | if (countryChecked.value) { 214 | selectedCountries.add(country) 215 | } else { 216 | selectedCountries.remove(country) 217 | } 218 | }, 219 | colors = CheckboxDefaults.colors( 220 | checkedColor = MaterialTheme.colors.primary, 221 | uncheckedColor = MaterialTheme.colors.primary 222 | ) 223 | ) 224 | Text( 225 | text = country.name, 226 | color = themeColors.primary, 227 | style = MaterialTheme.typography.body2 228 | ) 229 | } 230 | } 231 | } 232 | ) 233 | } 234 | } 235 | } 236 | 237 | @Preview 238 | @Composable 239 | fun FilterScreenPreview() { 240 | ThemedPreview { 241 | FiltersScreen( 242 | countyList = listOf( 243 | Country("US"), 244 | Country("UK"), 245 | Country("India"), 246 | Country("Germany"), 247 | Country("Japan"), 248 | Country("Poland") 249 | ), 250 | selectedCountries = mutableListOf(), 251 | onDismiss = {}, 252 | onApply = { _, _ -> } 253 | ) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/Header.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.R 4 | import `in`.jitinsharma.android.conf.utils.ThemedPreview 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun Header( 16 | modifier: Modifier = Modifier, 17 | onFilterClicked: () -> Unit, 18 | onAndroidIconClicked: () -> Unit, 19 | onSettingsClicked: () -> Unit, 20 | ) { 21 | Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { 22 | Box( 23 | Modifier.clickable( 24 | onClick = { onFilterClicked() }) 25 | ) { 26 | Image( 27 | modifier = Modifier.padding(top = 12.dp), 28 | painter = painterResource( 29 | id = R.drawable.ic_baseline_filter_list 30 | ), 31 | contentDescription = "Open Filter conference dialog" 32 | ) 33 | } 34 | Box( 35 | Modifier.clickable( 36 | onClick = { onAndroidIconClicked() }) 37 | ) { 38 | Image( 39 | painter = painterResource( 40 | id = R.drawable.ic_android_symbol_green 41 | ), 42 | contentDescription = "Open conference list page" 43 | ) 44 | } 45 | Box( 46 | Modifier.clickable( 47 | onClick = { onSettingsClicked() }) 48 | ) { 49 | Image( 50 | modifier = Modifier.padding(top = 12.dp), 51 | painter = painterResource( 52 | id = R.drawable.ic_baseline_settings 53 | ), 54 | contentDescription = "Open settings page" 55 | ) 56 | } 57 | } 58 | } 59 | 60 | @Preview 61 | @Composable 62 | fun HeaderPreview() { 63 | ThemedPreview { 64 | Header( 65 | onFilterClicked = {}, 66 | onAndroidIconClicked = {}, 67 | onSettingsClicked = {} 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/LoadingView.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.utils.ThemedPreview 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.CircularProgressIndicator 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.tooling.preview.Preview 13 | 14 | @Composable 15 | fun LoadingView() { 16 | Column( 17 | modifier = Modifier.fillMaxSize(), 18 | verticalArrangement = Arrangement.Center, 19 | horizontalAlignment = Alignment.CenterHorizontally 20 | ) { 21 | CircularProgressIndicator( 22 | color = MaterialTheme.colors.secondary 23 | ) 24 | } 25 | } 26 | 27 | @Preview 28 | @Composable 29 | fun LoadingViewPreview() { 30 | ThemedPreview { 31 | LoadingView() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.viewmodel.ConferenceViewModel 4 | import `in`.jitinsharma.android.conf.viewmodel.FilterScreenViewModel 5 | import android.os.Bundle 6 | import androidx.activity.compose.setContent 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.compose.material.MaterialTheme 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import org.koin.androidx.viewmodel.ext.android.viewModel 11 | 12 | @ExperimentalCoroutinesApi 13 | class MainActivity : AppCompatActivity() { 14 | 15 | private val conferenceViewModel by viewModel() 16 | private val filterScreenViewModel by viewModel() 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContent { 21 | MaterialTheme(colors = themeColors) { 22 | ConferenceApp( 23 | conferenceViewModel = conferenceViewModel, 24 | filterScreenViewModel = filterScreenViewModel 25 | ) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/SettingsPage.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.preferences.AppPreferences 4 | import `in`.jitinsharma.android.conf.utils.ThemedPreview 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.Checkbox 8 | import androidx.compose.material.Divider 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.collectAsState 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.rememberCoroutineScope 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import kotlinx.coroutines.launch 19 | import org.koin.java.KoinJavaComponent.getKoin 20 | 21 | @Composable 22 | fun SettingsPage() { 23 | val appPreferences = remember { getKoin().get() } 24 | Column( 25 | modifier = Modifier.fillMaxSize() 26 | ) { 27 | val conferenceUpdateNotificationState = 28 | appPreferences.getUpdatePreference().collectAsState(initial = false) 29 | val scope = rememberCoroutineScope() 30 | Text( 31 | text = "Notification Preferences", 32 | style = MaterialTheme.typography.h6, 33 | color = MaterialTheme.colors.secondary 34 | ) 35 | Divider(color = MaterialTheme.colors.secondary) 36 | Spacer(modifier = Modifier.height(16.dp)) 37 | Row( 38 | horizontalArrangement = Arrangement.SpaceBetween, 39 | modifier = Modifier 40 | .fillMaxWidth() 41 | .clickable( 42 | onClick = { 43 | scope.launch { 44 | appPreferences.setUpdatePreference(conferenceUpdateNotificationState.value.not()) 45 | } 46 | }) 47 | ) { 48 | Text( 49 | text = "Conference Update Notification", 50 | style = MaterialTheme.typography.body1, 51 | color = MaterialTheme.colors.secondary 52 | ) 53 | Checkbox( 54 | checked = conferenceUpdateNotificationState.value, 55 | onCheckedChange = { 56 | scope.launch { 57 | appPreferences.setUpdatePreference( 58 | conferenceUpdateNotificationState.value.not() 59 | ) 60 | } 61 | }) 62 | } 63 | } 64 | } 65 | 66 | @Preview 67 | @Composable 68 | fun NotificationPagePreview() { 69 | ThemedPreview { 70 | SettingsPage() 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/Themes.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import androidx.compose.material.lightColors 4 | import androidx.compose.ui.graphics.Color 5 | 6 | val themeColors = lightColors( 7 | primary = Color(0xFF092432), 8 | primaryVariant = Color(0xFF092432), 9 | secondary = Color(0xFF3DDB85), 10 | onSurface = Color(0xFF3DDB85) 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/ui/WtfView.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.ui 2 | 3 | import `in`.jitinsharma.android.conf.utils.ThemedPreview 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.Button 9 | import androidx.compose.material.ButtonDefaults 10 | import androidx.compose.material.MaterialTheme 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.text.SpanStyle 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.buildAnnotatedString 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | 23 | @Composable 24 | fun WtfView( 25 | onRetryClick: () -> Unit 26 | ) { 27 | Column( 28 | modifier = Modifier.fillMaxSize(), 29 | verticalArrangement = Arrangement.Center, 30 | horizontalAlignment = Alignment.CenterHorizontally 31 | ) { 32 | Text( 33 | text = buildAnnotatedString { 34 | pushStyle(SpanStyle(color = Color(0xFFCEECFD))) 35 | append("{") 36 | pop() 37 | append(" WTF ") 38 | pushStyle(SpanStyle(color = Color(0xFFCEECFD))) 39 | append("}") 40 | pop() 41 | }, 42 | color = MaterialTheme.colors.secondary, 43 | style = TextStyle(fontSize = 32.sp) 44 | ) 45 | Button( 46 | modifier = Modifier.padding(top = 8.dp), 47 | colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary), 48 | onClick = { onRetryClick() }) { 49 | Text( 50 | text = "Retry", 51 | color = MaterialTheme.colors.primary 52 | ) 53 | } 54 | } 55 | } 56 | 57 | @Preview 58 | @Composable 59 | fun WtfViewPreview() { 60 | ThemedPreview { 61 | WtfView(onRetryClick = {}) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/utils/DispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.utils 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | interface DispatcherProvider { 7 | 8 | val io: CoroutineDispatcher get() = Dispatchers.IO 9 | val main: CoroutineDispatcher get() = Dispatchers.Main 10 | val default: CoroutineDispatcher get() = Dispatchers.Default 11 | 12 | } 13 | 14 | object AppDispatchers : DispatcherProvider -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/utils/PreviewUtils.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.utils 2 | 3 | import `in`.jitinsharma.android.conf.ui.themeColors 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.material.Colors 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | 11 | @Composable 12 | internal fun ThemedPreview( 13 | colors: Colors = themeColors, 14 | content: @Composable () -> Unit 15 | ) { 16 | MaterialTheme(colors = colors) { 17 | Box(modifier = Modifier.background(color = MaterialTheme.colors.primary)) { 18 | content() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/viewmodel/ConferenceViewModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.viewmodel 2 | 3 | import `in`.jitinsharma.android.conf.model.ConferenceData 4 | import `in`.jitinsharma.android.conf.model.Country 5 | import `in`.jitinsharma.android.conf.repository.ConferenceRepository 6 | import `in`.jitinsharma.android.conf.utils.AppDispatchers 7 | import `in`.jitinsharma.android.conf.utils.DispatcherProvider 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.flow.catch 13 | import kotlinx.coroutines.flow.collect 14 | import kotlinx.coroutines.launch 15 | 16 | class ConferenceViewModel( 17 | private val conferenceRepository: ConferenceRepository, 18 | private val dispatcherProvider: DispatcherProvider = AppDispatchers 19 | ) : ViewModel() { 20 | 21 | private var _uiState: MutableStateFlow = 22 | MutableStateFlow(ConferenceListUiState.Loading) 23 | val uiState: StateFlow = _uiState 24 | private var originalConferenceList = listOf() 25 | 26 | init { 27 | loadConferences() 28 | } 29 | 30 | fun loadConferences() { 31 | viewModelScope.launch(dispatcherProvider.io) { 32 | conferenceRepository.getConferenceDataList() 33 | .catch { 34 | _uiState.value = ConferenceListUiState.Error 35 | } 36 | .collect { conferenceDataList -> 37 | 38 | if (conferenceDataList.isNotEmpty()) { 39 | _uiState.value = ConferenceListUiState.Success(conferenceDataList) 40 | originalConferenceList = conferenceDataList 41 | } 42 | 43 | try { 44 | conferenceRepository.loadConferenceData() 45 | } catch (e: Exception) { 46 | if (_uiState.value == ConferenceListUiState.Loading) { 47 | _uiState.value = ConferenceListUiState.Error 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | fun filterConferences(cfpFilterChecked: Boolean, selectedCountries: List) { 55 | if (!cfpFilterChecked && selectedCountries.isEmpty()) { 56 | _uiState.value = ConferenceListUiState.Success(originalConferenceList) 57 | } else { 58 | val filteredConferences = originalConferenceList.filter { 59 | if (cfpFilterChecked) { 60 | it.cfpData != null && it.cfpData!!.isCfpActive 61 | } else { 62 | true 63 | } 64 | }.filter { 65 | selectedCountries.contains(Country(it.country)) 66 | } 67 | _uiState.value = ConferenceListUiState.Success(filteredConferences) 68 | } 69 | } 70 | } 71 | 72 | sealed class ConferenceListUiState { 73 | class Success(val conferenceList: List) : ConferenceListUiState() 74 | object Loading : ConferenceListUiState() 75 | object Error : ConferenceListUiState() 76 | } -------------------------------------------------------------------------------- /app/src/main/java/in/jitinsharma/android/conf/viewmodel/FilterScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.jitinsharma.android.conf.viewmodel 2 | 3 | import `in`.jitinsharma.android.conf.model.Country 4 | import `in`.jitinsharma.android.conf.repository.ConferenceRepository 5 | import androidx.collection.ArraySet 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.flow.collect 12 | import kotlinx.coroutines.launch 13 | 14 | class FilterScreenViewModel( 15 | private val conferenceRepository: ConferenceRepository 16 | ) : ViewModel() { 17 | 18 | private var _uiState = 19 | MutableStateFlow(FilterScreenUiState.Success()) 20 | val uiState: StateFlow = _uiState 21 | 22 | init { 23 | loadCountries() 24 | } 25 | 26 | private fun loadCountries() { 27 | viewModelScope.launch(Dispatchers.IO) { 28 | conferenceRepository.getConferenceDataList().collect { conferenceDataList -> 29 | val countries = conferenceDataList.mapTo(ArraySet()) { countryName -> 30 | Country(name = countryName.country) 31 | }.toList() 32 | _uiState.value = FilterScreenUiState.Success(countryList = countries) 33 | } 34 | } 35 | } 36 | 37 | fun updateUiState(cfpFilterChecked: Boolean, selectedCountries: List) { 38 | val countries = _uiState.value.countryList 39 | _uiState.value = FilterScreenUiState.Success(cfpFilterChecked, selectedCountries, countries) 40 | } 41 | } 42 | 43 | sealed class FilterScreenUiState { 44 | class Success( 45 | val cfpFilterChecked: Boolean = false, 46 | val selectedCountries: List = emptyList(), 47 | val countryList: List = emptyList() 48 | ) : FilterScreenUiState() 49 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_android_symbol_green.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_alarm_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_calendar_today.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_filter_list.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_location_on.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitinsharma/android-conferences/16ca90a0adee2dbbe840fa17ae2d916d4c6f4c2e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #BB86FC 4 | #6200EE 5 | #3700B3 6 | #03DAC5 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Android Conferences 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 |