From 92c1463a4d43f19a685fa6e18275533065d97602 Mon Sep 17 00:00:00 2001 From: jordanrsas Date: Thu, 18 Jul 2024 20:18:33 -0600 Subject: [PATCH 1/8] First commit: Create Clean Architecture directory structure. BugFix, app crashes. [x] Add MyApplication class to XML Manifest file. [x] Add Room DataBase dependencies to build.gradle [x] Add Navigation Architecture dependencies to build.gradle --- .gitignore | 49 +++++++++++++------ app/build.gradle | 22 +++++++-- app/src/main/AndroidManifest.xml | 3 +- .../example/otchallenge/di/AppComponent.kt | 2 +- .../{ => presentation/view}/MainActivity.kt | 4 +- app/src/main/res/layout/activity_main.xml | 2 +- gradle/libs.versions.toml | 18 +++++-- 7 files changed, 72 insertions(+), 28 deletions(-) rename app/src/main/java/com/example/otchallenge/{ => presentation/view}/MainActivity.kt (84%) diff --git a/.gitignore b/.gitignore index 5bd175f..82c3310 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,35 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) local.properties -.idea + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +**/ApiKeys.kt \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 0d0fa1b..d9c4a4e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,28 +34,40 @@ android { } dependencies { - implementation libs.androidx.core.ktx implementation libs.androidx.appcompat implementation libs.material implementation libs.androidx.activity implementation libs.androidx.constraintlayout - // dagger + + //Dagger implementation libs.dagger kapt libs.dagger.compiler - //retrofit + //Retrofit implementation libs.retrofit + implementation libs.retrofit.gson implementation libs.retrofit.rx.adapter - //glide + //Glide implementation libs.glide + kapt libs.glide.compiler - //reactive x + //Reactive x implementation libs.rx.android implementation libs.rx.java implementation libs.rx.kotlin + //Room database + implementation libs.androidx.room + kapt libs.androidx.room.compiler + implementation libs.androidx.room.rxjava + + //Navigation Architecture Component + implementation libs.androidx.navigation + implementation libs.androidx.navigation.ui + + //Testing testImplementation libs.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espresso.core diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a18c7a..9aa3430 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ diff --git a/app/src/main/java/com/example/otchallenge/di/AppComponent.kt b/app/src/main/java/com/example/otchallenge/di/AppComponent.kt index d3c83c6..f78defa 100644 --- a/app/src/main/java/com/example/otchallenge/di/AppComponent.kt +++ b/app/src/main/java/com/example/otchallenge/di/AppComponent.kt @@ -1,6 +1,6 @@ package com.example.otchallenge.di -import com.example.otchallenge.MainActivity +import com.example.otchallenge.presentation.view.MainActivity import dagger.Component import javax.inject.Singleton diff --git a/app/src/main/java/com/example/otchallenge/MainActivity.kt b/app/src/main/java/com/example/otchallenge/presentation/view/MainActivity.kt similarity index 84% rename from app/src/main/java/com/example/otchallenge/MainActivity.kt rename to app/src/main/java/com/example/otchallenge/presentation/view/MainActivity.kt index d35da32..6fd1793 100644 --- a/app/src/main/java/com/example/otchallenge/MainActivity.kt +++ b/app/src/main/java/com/example/otchallenge/presentation/view/MainActivity.kt @@ -1,10 +1,12 @@ -package com.example.otchallenge +package com.example.otchallenge.presentation.view import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import com.example.otchallenge.MyApplication +import com.example.otchallenge.R class MainActivity : AppCompatActivity() { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c7a2d54..887237f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,7 +5,7 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".presentation.view.MainActivity"> Date: Fri, 19 Jul 2024 00:32:24 -0600 Subject: [PATCH 2/8] Add Data layer [x] Api, consume NYT Books Service, [x] Data Base Module [x] Repository [x] Create NYT Books Service Data Response Model Domain Layer [x] GetBooks Use Case Di [x] Application Component [x] App Module Also create NetworkHelper to check connectivity Add Network State permission to AndroidManifest.xml --- app/src/main/AndroidManifest.xml | 1 + .../otchallenge/data/api/BooksService.kt | 13 +++++++ .../otchallenge/data/database/BookDao.kt | 17 +++++++++ .../otchallenge/data/database/BookDataBase.kt | 10 ++++++ .../otchallenge/data/di/DatabaseModule.kt | 26 ++++++++++++++ .../otchallenge/data/di/NetworkModule.kt | 35 +++++++++++++++++++ .../otchallenge/data/di/RepositoryModule.kt | 20 +++++++++++ .../example/otchallenge/data/model/Book.kt | 34 ++++++++++++++++++ .../example/otchallenge/data/model/BuyLink.kt | 6 ++++ .../otchallenge/data/model/Correction.kt | 6 ++++ .../example/otchallenge/data/model/Isbn.kt | 6 ++++ .../data/model/OverviewResponse.kt | 10 ++++++ .../example/otchallenge/data/model/Results.kt | 18 ++++++++++ .../data/repository/BookRepository.kt | 22 ++++++++++++ .../example/otchallenge/di/AppComponent.kt | 7 ++-- .../com/example/otchallenge/di/AppModule.kt | 26 ++++++++++++++ .../domain/usecase/GetBooksUseCase.kt | 10 ++++++ .../otchallenge/utils/NetworkHelper.kt | 21 +++++++++++ .../otchallenge/utils/NetworkInterceptor.kt | 30 ++++++++++++++++ 19 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/example/otchallenge/data/api/BooksService.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/database/BookDao.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/database/BookDataBase.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/di/DatabaseModule.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/di/NetworkModule.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/di/RepositoryModule.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/model/Book.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/model/BuyLink.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/model/Correction.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/model/Isbn.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/model/OverviewResponse.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/model/Results.kt create mode 100644 app/src/main/java/com/example/otchallenge/data/repository/BookRepository.kt create mode 100644 app/src/main/java/com/example/otchallenge/di/AppModule.kt create mode 100644 app/src/main/java/com/example/otchallenge/domain/usecase/GetBooksUseCase.kt create mode 100644 app/src/main/java/com/example/otchallenge/utils/NetworkHelper.kt create mode 100644 app/src/main/java/com/example/otchallenge/utils/NetworkInterceptor.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9aa3430..09e89ab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/database/BookDao.kt b/app/src/main/java/com/example/otchallenge/data/database/BookDao.kt new file mode 100644 index 0000000..6fde937 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/database/BookDao.kt @@ -0,0 +1,17 @@ +package com.example.otchallenge.data.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.example.otchallenge.data.model.Book +import io.reactivex.Single + +@Dao +interface BookDao { + @Query("SELECT * FROM books") + fun getBooks(): Single> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertBooks(books: List) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/database/BookDataBase.kt b/app/src/main/java/com/example/otchallenge/data/database/BookDataBase.kt new file mode 100644 index 0000000..a38862e --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/database/BookDataBase.kt @@ -0,0 +1,10 @@ +package com.example.otchallenge.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.example.otchallenge.data.model.Book + +@Database(entities = [Book::class], version = 1) +abstract class BookDatabase : RoomDatabase() { + abstract fun bookDao(): BookDao +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/di/DatabaseModule.kt b/app/src/main/java/com/example/otchallenge/data/di/DatabaseModule.kt new file mode 100644 index 0000000..cad081a --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/di/DatabaseModule.kt @@ -0,0 +1,26 @@ +package com.example.otchallenge.data.di + +import android.app.Application +import androidx.room.Room +import com.example.otchallenge.data.database.BookDao +import com.example.otchallenge.data.database.BookDatabase +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class DatabaseModule { + @Provides + @Singleton + fun provideDatabase(application: Application): BookDatabase { + return Room.databaseBuilder(application, BookDatabase::class.java, "books.db") + .fallbackToDestructiveMigration() + .build() + } + + @Provides + @Singleton + fun provideBookDao(database: BookDatabase): BookDao { + return database.bookDao() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/di/NetworkModule.kt b/app/src/main/java/com/example/otchallenge/data/di/NetworkModule.kt new file mode 100644 index 0000000..35d0125 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/di/NetworkModule.kt @@ -0,0 +1,35 @@ +package com.example.otchallenge.data.di + +import com.example.otchallenge.data.api.BooksService +import com.example.otchallenge.utils.NetworkInterceptor +import dagger.Module +import dagger.Provides +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +class NetworkModule { + @Provides + @Singleton + fun provideRetrofit(networkInterceptor: NetworkInterceptor): Retrofit { + val client = OkHttpClient.Builder() + .addInterceptor(networkInterceptor) + .build() + + return Retrofit.Builder() + .baseUrl("https://api.nytimes.com/svc/books/v3/") + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + } + + @Provides + @Singleton + fun provideBookService(retrofit: Retrofit): BooksService { + return retrofit.create(BooksService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/di/RepositoryModule.kt b/app/src/main/java/com/example/otchallenge/data/di/RepositoryModule.kt new file mode 100644 index 0000000..c0dc377 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/di/RepositoryModule.kt @@ -0,0 +1,20 @@ +package com.example.otchallenge.data.di + +import com.example.otchallenge.data.api.BooksService +import com.example.otchallenge.data.database.BookDao +import com.example.otchallenge.data.repository.BookRepository +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class RepositoryModule { + @Provides + @Singleton + fun provideBookRepository( + bookService: BooksService, + bookDao: BookDao + ): BookRepository { + return BookRepository(bookService, bookDao) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/model/Book.kt b/app/src/main/java/com/example/otchallenge/data/model/Book.kt new file mode 100644 index 0000000..768fe5c --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/model/Book.kt @@ -0,0 +1,34 @@ +package com.example.otchallenge.data.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName + +@Entity(tableName = "books") +data class Book( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val rank: Int, + @SerializedName("rank_last_week") val rankLastWeek: Int, + @SerializedName("weeks_on_list") val weeksOnList: Int, + val asterisk: Int, + val dagger: Int, + @SerializedName("primary_isbn10") val primaryIsbn10: String, + @SerializedName("primary_isbn13") val primaryIsbn13: String, + val publisher: String, + val description: String, + val price: String, + val title: String, + val author: String, + val contributor: String, + @SerializedName("contributor_note") val contributorNote: String, + @SerializedName("book_image") val bookImage: String, + @SerializedName("book_image_width") val bookImageWidth: Int, + @SerializedName("book_image_height") val bookImageHeight: Int, + @SerializedName("amazon_product_url") val amazonProductUrl: String, + @SerializedName("age_group") val ageGroup: String, + @SerializedName("book_review_link") val bookReviewLink: String, + @SerializedName("first_chapter_link") val firstChapterLink: String, + @SerializedName("sunday_review_link") val sundayReviewLink: String, + @SerializedName("article_chapter_link") val articleChapterLink: String, + val bookUri: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/model/BuyLink.kt b/app/src/main/java/com/example/otchallenge/data/model/BuyLink.kt new file mode 100644 index 0000000..f96264c --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/model/BuyLink.kt @@ -0,0 +1,6 @@ +package com.example.otchallenge.data.model + +data class BuyLink( + val name: String, + val url: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/model/Correction.kt b/app/src/main/java/com/example/otchallenge/data/model/Correction.kt new file mode 100644 index 0000000..2e75040 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/model/Correction.kt @@ -0,0 +1,6 @@ +package com.example.otchallenge.data.model + +data class Correction( + val article: String?, + val original: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/model/Isbn.kt b/app/src/main/java/com/example/otchallenge/data/model/Isbn.kt new file mode 100644 index 0000000..9c8a9d2 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/model/Isbn.kt @@ -0,0 +1,6 @@ +package com.example.otchallenge.data.model + +data class Isbn( + val isbn10: String, + val isbn13: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/model/OverviewResponse.kt b/app/src/main/java/com/example/otchallenge/data/model/OverviewResponse.kt new file mode 100644 index 0000000..c9cf90e --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/model/OverviewResponse.kt @@ -0,0 +1,10 @@ +package com.example.otchallenge.data.model + +import com.google.gson.annotations.SerializedName + +data class OverviewResponse( + val status: String, + val copyright: String, + @SerializedName("num_results") val numResults: Int, + val results: Results +) diff --git a/app/src/main/java/com/example/otchallenge/data/model/Results.kt b/app/src/main/java/com/example/otchallenge/data/model/Results.kt new file mode 100644 index 0000000..10a9853 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/model/Results.kt @@ -0,0 +1,18 @@ +package com.example.otchallenge.data.model + +import com.google.gson.annotations.SerializedName + +data class Results( + @SerializedName("list_name") val listName: String, + @SerializedName("list_name_encoded") val listNameEncoded: String, + @SerializedName("bestsellers_date") val bestsellersDate: String, + @SerializedName("published_date") val publishedDate: String, + @SerializedName("published_date_description") val publishedDateDescription: String, + @SerializedName("next_published_date") val nextPublishedDate: String, + @SerializedName("previous_published_date") val previousPublishedDate: String, + @SerializedName("display_name") val displayName: String, + @SerializedName("normal_list_ends_at") val normalListEndsAt: Int, + val updated: String, + val books: List, + val corrections: List? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/data/repository/BookRepository.kt b/app/src/main/java/com/example/otchallenge/data/repository/BookRepository.kt new file mode 100644 index 0000000..2e72619 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/data/repository/BookRepository.kt @@ -0,0 +1,22 @@ +package com.example.otchallenge.data.repository + +import com.example.otchallenge.data.api.BooksService +import com.example.otchallenge.data.database.BookDao +import com.example.otchallenge.data.model.Book +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class BookRepository @Inject constructor( + private val bookService: BooksService, + private val bookDao: BookDao +) { + fun getBooks(apiKey: String): Single> { + return bookService.getBooks(apiKey) + .map { it.results.books } + .doOnSuccess { bookDao.insertBooks(it) } + .onErrorResumeNext { + bookDao.getBooks().subscribeOn(Schedulers.io()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/di/AppComponent.kt b/app/src/main/java/com/example/otchallenge/di/AppComponent.kt index f78defa..ff73678 100644 --- a/app/src/main/java/com/example/otchallenge/di/AppComponent.kt +++ b/app/src/main/java/com/example/otchallenge/di/AppComponent.kt @@ -1,11 +1,14 @@ package com.example.otchallenge.di +import com.example.otchallenge.data.di.DatabaseModule +import com.example.otchallenge.data.di.NetworkModule +import com.example.otchallenge.data.di.RepositoryModule import com.example.otchallenge.presentation.view.MainActivity import dagger.Component import javax.inject.Singleton @Singleton -@Component +@Component(modules = [AppModule::class, NetworkModule::class, DatabaseModule::class, RepositoryModule::class]) interface AppComponent { - fun inject(activity: MainActivity) + fun inject(activity: MainActivity) } diff --git a/app/src/main/java/com/example/otchallenge/di/AppModule.kt b/app/src/main/java/com/example/otchallenge/di/AppModule.kt new file mode 100644 index 0000000..0224ee7 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/di/AppModule.kt @@ -0,0 +1,26 @@ +package com.example.otchallenge.di + +import android.app.Application +import android.content.Context +import com.example.otchallenge.utils.ConnectivityChecker +import com.example.otchallenge.utils.NetworkHelper + +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class AppModule(private val application: Application) { + @Provides + @Singleton + fun provideApplication(): Application = application + + @Provides + @Singleton + fun provideContext(): Context = application.applicationContext + + @Provides + @Singleton + fun provideConnectivityChecker(networkHelper: NetworkHelper): ConnectivityChecker = + networkHelper +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/domain/usecase/GetBooksUseCase.kt b/app/src/main/java/com/example/otchallenge/domain/usecase/GetBooksUseCase.kt new file mode 100644 index 0000000..e7429fd --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/domain/usecase/GetBooksUseCase.kt @@ -0,0 +1,10 @@ +package com.example.otchallenge.domain.usecase + +import com.example.otchallenge.data.model.Book +import com.example.otchallenge.data.repository.BookRepository +import io.reactivex.Single +import javax.inject.Inject + +class GetBooksUseCase @Inject constructor(private val repository: BookRepository) { + fun execute(apiKey: String): Single> = repository.getBooks(apiKey) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/utils/NetworkHelper.kt b/app/src/main/java/com/example/otchallenge/utils/NetworkHelper.kt new file mode 100644 index 0000000..106c739 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/utils/NetworkHelper.kt @@ -0,0 +1,21 @@ +package com.example.otchallenge.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import javax.inject.Inject + +interface ConnectivityChecker { + fun isNetworkConnected(): Boolean +} + +class NetworkHelper @Inject constructor(private val context: Context) : ConnectivityChecker { + override fun isNetworkConnected(): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) + return networkCapabilities != null && + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/utils/NetworkInterceptor.kt b/app/src/main/java/com/example/otchallenge/utils/NetworkInterceptor.kt new file mode 100644 index 0000000..ee6f742 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/utils/NetworkInterceptor.kt @@ -0,0 +1,30 @@ +package com.example.otchallenge.utils + +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException +import java.net.SocketTimeoutException +import javax.inject.Inject + +class NetworkInterceptor @Inject constructor(private val connectivityChecker: ConnectivityChecker) : + Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + if (!connectivityChecker.isNetworkConnected()) { + throw NoConnectivityException() + } + + val request = chain.request() + return try { + chain.proceed(request) + } catch (e: SocketTimeoutException) { + throw TimeoutException() + } catch (e: IOException) { + throw NetworkException() + } + } +} + +class NoConnectivityException : IOException("No connectivity exception") +class TimeoutException : IOException("Timeout exception") +class NetworkException : IOException("Network exception") \ No newline at end of file From 104649539218f0cd0ba82cad27e08f752a45ba11 Mon Sep 17 00:00:00 2001 From: jordanrsas Date: Sat, 20 Jul 2024 13:10:51 -0600 Subject: [PATCH 3/8] Add Navigation Architecture and create fragments. [x] SplashFragment [x] BookDetailFragment [x] BookListFragment [x] NavGraph [x] Fade animation for transition [ ] Book list [ ] Book detail [ ] Presentation classes [ ] View abstraction [ ] Di --- app/build.gradle | 1 + .../presentation/view/BookDetailFragment.kt | 20 ++++++ .../presentation/view/BookListFragment.kt | 20 ++++++ .../presentation/view/SplashFragment.kt | 29 ++++++++ app/src/main/res/anim/fade_in.xml | 5 ++ app/src/main/res/anim/fade_out.xml | 5 ++ app/src/main/res/drawable/nyt_logo.png | Bin 0 -> 6371 bytes app/src/main/res/layout/activity_main.xml | 39 ++++++----- .../main/res/layout/fragment_book_detail.xml | 66 ++++++++++++++++++ .../main/res/layout/fragment_book_list.xml | 41 +++++++++++ app/src/main/res/layout/fragment_splash.xml | 19 +++++ app/src/main/res/navigation/nav_graph.xml | 37 ++++++++++ gradle/libs.versions.toml | 2 + 13 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/example/otchallenge/presentation/view/BookDetailFragment.kt create mode 100644 app/src/main/java/com/example/otchallenge/presentation/view/BookListFragment.kt create mode 100644 app/src/main/java/com/example/otchallenge/presentation/view/SplashFragment.kt create mode 100644 app/src/main/res/anim/fade_in.xml create mode 100644 app/src/main/res/anim/fade_out.xml create mode 100644 app/src/main/res/drawable/nyt_logo.png create mode 100644 app/src/main/res/layout/fragment_book_detail.xml create mode 100644 app/src/main/res/layout/fragment_book_list.xml create mode 100644 app/src/main/res/layout/fragment_splash.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml diff --git a/app/build.gradle b/app/build.gradle index d9c4a4e..1e9dba2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation libs.material implementation libs.androidx.activity implementation libs.androidx.constraintlayout + implementation libs.androidx.swiperefreshlayout //Dagger implementation libs.dagger diff --git a/app/src/main/java/com/example/otchallenge/presentation/view/BookDetailFragment.kt b/app/src/main/java/com/example/otchallenge/presentation/view/BookDetailFragment.kt new file mode 100644 index 0000000..4ca2a61 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/presentation/view/BookDetailFragment.kt @@ -0,0 +1,20 @@ +package com.example.otchallenge.presentation.view + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.example.otchallenge.R + +class BookDetailFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_book_detail, container, false) + return view + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/presentation/view/BookListFragment.kt b/app/src/main/java/com/example/otchallenge/presentation/view/BookListFragment.kt new file mode 100644 index 0000000..850f6d4 --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/presentation/view/BookListFragment.kt @@ -0,0 +1,20 @@ +package com.example.otchallenge.presentation.view + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.example.otchallenge.R + +class BookListFragment: Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_book_list, container, false) + return view + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/otchallenge/presentation/view/SplashFragment.kt b/app/src/main/java/com/example/otchallenge/presentation/view/SplashFragment.kt new file mode 100644 index 0000000..428c67f --- /dev/null +++ b/app/src/main/java/com/example/otchallenge/presentation/view/SplashFragment.kt @@ -0,0 +1,29 @@ +package com.example.otchallenge.presentation.view + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.example.otchallenge.R + +class SplashFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_splash, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + Handler(Looper.getMainLooper()).postDelayed({ + findNavController().navigate(R.id.action_splashFragment_to_bookListFragment) + }, 3000) // Delay for 3seconds + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..2477173 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 0000000..46227cc --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/nyt_logo.png b/app/src/main/res/drawable/nyt_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..581a262b0fc2264d5f02b0aa83f00cf3ebca31d8 GIT binary patch literal 6371 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 887237f..44630f9 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,26 @@ - + - + - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_book_detail.xml b/app/src/main/res/layout/fragment_book_detail.xml new file mode 100644 index 0000000..501a8b8 --- /dev/null +++ b/app/src/main/res/layout/fragment_book_detail.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + +