diff --git a/EpicDatabseExample/.idea/gradle.xml b/EpicDatabseExample/.idea/gradle.xml index 4e3844e..a2d7c21 100644 --- a/EpicDatabseExample/.idea/gradle.xml +++ b/EpicDatabseExample/.idea/gradle.xml @@ -1,5 +1,6 @@ + diff --git a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/NoteDatabase.kt b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/NoteDatabase.kt index 1c213b2..7d0fad8 100644 --- a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/NoteDatabase.kt +++ b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/NoteDatabase.kt @@ -39,16 +39,29 @@ abstract class NoteDatabase : RoomDatabase() { private fun migrationList() = arrayOf( object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE $PERSON_TABLE (" + - "${PersonEntity.COLUMN_NAME} TEXT NOT NULL," + - "PRIMARY KEY(${PersonEntity.COLUMN_NAME})" + - ")") - database.execSQL("ALTER TABLE $NOTE_TABLE " + - "ADD COLUMN ${NoteEntity.COLUMN_PERSON_NAME} TEXT NOT NULL " + - "DEFAULT ('${PersonEntity.DEFAULT_PERSON_NAME}')") + database.execSQL( + "CREATE TABLE $PERSON_TABLE (" + + "${PersonEntity.COLUMN_NAME} TEXT NOT NULL," + + "PRIMARY KEY(${PersonEntity.COLUMN_NAME})" + + ")" + ) + database.execSQL( + "ALTER TABLE $NOTE_TABLE " + + "ADD COLUMN ${NoteEntity.COLUMN_PERSON_NAME} TEXT NOT NULL " + + "DEFAULT ('${PersonEntity.DEFAULT_PERSON_NAME}')" + ) } }, + // TODO Добавить миграцию. + object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "ALTER TABLE $NOTE_TABLE ADD COLUMN ${NoteEntity.COLUMN_IS_COMPLETED} INTEGER NOT NULL DEFAULT(0)" + ) + } + }, + ) private fun createDatabaseInstance(application: Application): NoteDatabase { diff --git a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteDao.kt b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteDao.kt index 8645616..b095687 100644 --- a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteDao.kt +++ b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteDao.kt @@ -30,4 +30,6 @@ abstract class NoteDao { // Что можно указать в качестве возвращаемого типа? // Напомню, что выше есть метод observeNoteList, который будет вызываться при любом // изменении списка в Базе Данных. + @Update(onConflict = OnConflictStrategy.REPLACE) + abstract fun updateNote(note: NoteEntity): Completable } \ No newline at end of file diff --git a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteEntity.kt b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteEntity.kt index b42b9b3..ecef448 100644 --- a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteEntity.kt +++ b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/db/note/NoteEntity.kt @@ -26,6 +26,8 @@ data class NoteEntity( // В SQLite не поддерживается Boolean, но в Room его можно использовать. // Поэтому поле делаем именно Boolean, а при добавлении миграции - нужно будет погуглить, // как обойти это ограничение. + @ColumnInfo(name = COLUMN_IS_COMPLETED) + var isCompleted: Boolean = false, ) { companion object { @@ -33,5 +35,6 @@ data class NoteEntity( const val COLUMN_TITLE = "title" const val COLUMN_DESCRIPTION = "descriptions" const val COLUMN_PERSON_NAME = "person_name" + const val COLUMN_IS_COMPLETED = "is_completed" } } \ No newline at end of file diff --git a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListAdapter.kt b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListAdapter.kt index 192174f..d172523 100644 --- a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListAdapter.kt +++ b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListAdapter.kt @@ -21,14 +21,14 @@ class NoteListAdapter( } holder.personName.text = note.personName // TODO Раскомментировать, после добавления нового поля в NoteEntity. -// holder.isCompleted.setOnClickListener { -// onIsCompletedClick.invoke(note.copy( -// isCompleted = !note.isCompleted -// )) -// } -// val isCompletedChanged = note.isCompleted != holder.isCompleted.isChecked -// if (isCompletedChanged) { -// holder.isCompleted.isChecked = note.isCompleted -// } + holder.isCompleted.setOnClickListener { + onIsCompletedClick.invoke(note.copy( + isCompleted = !note.isCompleted + )) + } + val isCompletedChanged = note.isCompleted != holder.isCompleted.isChecked + if (isCompletedChanged) { + holder.isCompleted.isChecked = note.isCompleted + } } } \ No newline at end of file diff --git a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListViewModel.kt b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListViewModel.kt index 3344f2c..a3248b5 100644 --- a/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListViewModel.kt +++ b/EpicDatabseExample/app/src/main/java/com/example/epicdatabaseexample/ui/note_list/NoteListViewModel.kt @@ -65,6 +65,12 @@ class NoteListViewModel constructor( fun onIsCompletedClick(note: NoteEntity) { // TODO Добавить вызов noteDao для обновления элемента в таблице. + compositeDisposable.add( + noteDao.updateNote(note) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe() + ) } override fun onCleared() { diff --git a/EpicDatabseExample/build.gradle b/EpicDatabseExample/build.gradle index 8b75098..2c4a027 100644 --- a/EpicDatabseExample/build.gradle +++ b/EpicDatabseExample/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.1.2' apply false - id 'com.android.library' version '7.1.2' apply false + id 'com.android.application' version '7.2.0' apply false + id 'com.android.library' version '7.2.0' apply false id 'org.jetbrains.kotlin.android' version '1.5.30' apply false } diff --git a/EpicDatabseExample/gradle/wrapper/gradle-wrapper.properties b/EpicDatabseExample/gradle/wrapper/gradle-wrapper.properties index f724c9d..40b0bc3 100644 --- a/EpicDatabseExample/gradle/wrapper/gradle-wrapper.properties +++ b/EpicDatabseExample/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Apr 04 16:39:17 MSK 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/HomeWorkNetwork/app/build.gradle b/HomeWorkNetwork/app/build.gradle index d5e6ad0..a28ec12 100644 --- a/HomeWorkNetwork/app/build.gradle +++ b/HomeWorkNetwork/app/build.gradle @@ -8,10 +8,7 @@ android { compileSdk 31 defaultConfig { - - buildConfigField "String", "API_BASE_URL", '"api.themoviedb.org"' - buildConfigField "String", "API_KEY", '"/*ваш ap_key*/"' - buildConfigField "String", "API_IMAGE_BASE_URL", '"https://image.tmdb.org/t/p/w500"' + buildConfigField "String", "API_BASE_URL", '"reqres.in"' applicationId "com.pg.homeworknetwork" minSdk 30 @@ -35,6 +32,10 @@ android { kotlinOptions { jvmTarget = '1.8' } + + packagingOptions { + resources.excludes.add("META-INF/*") + } } dependencies { @@ -46,4 +47,19 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + + implementation 'io.ktor:ktor-client-core:1.6.8' + implementation 'io.ktor:ktor-client-android:1.6.8' + implementation 'io.ktor:ktor-client-serialization:1.6.8' + implementation 'io.ktor:ktor-client-logging:1.6.8' + implementation 'io.ktor:ktor-client-cio:1.6.8' + implementation 'io.ktor:ktor-server-core:1.6.8' + implementation 'io.ktor:ktor-server-netty:1.6.8' + + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" + + implementation 'com.squareup.okhttp3:logging-interceptor:4.8.1' + + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' } \ No newline at end of file diff --git a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Models.kt b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Models.kt index 69df05a..fbe661f 100644 --- a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Models.kt +++ b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Models.kt @@ -1,5 +1,64 @@ package com.pg.homeworknetwork -class Movies +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName -class Movie \ No newline at end of file +//{ +// "page": 1, +// "per_page": 6, +// "total": 12, +// "total_pages": 2, +// "data": [ +// { +// "id": 7, +// "email": "michael.lawson@reqres.in", +// "first_name": "Michael", +// "last_name": "Lawson", +// "avatar": "https://reqres.in/img/faces/7-image.jpg" +// }, +// ], +// "support": { +// "url": "https://reqres.in/#support-heading", +// "text": "To keep ReqRes free, contributions towards server costs are appreciated!" +//} +//} +@Serializable +data class Users( + @SerialName("page") + val page: Int, + @SerialName("data") + val data: List = ArrayList() +) + +//{ +// "data": { +// "id": 2, +// "email": "janet.weaver@reqres.in", +// "first_name": "Janet", +// "last_name": "Weaver", +// "avatar": "https://reqres.in/img/faces/2-image.jpg" +//}, +// "support": { +// "url": "https://reqres.in/#support-heading", +// "text": "To keep ReqRes free, contributions towards server costs are appreciated!" +//} +//} +@Serializable +data class Data( + @SerialName("data") + val data: User +) + +@Serializable +data class User( + @SerialName("id") + val id: Int, + @SerialName("email") + val email: String? = null, + @SerialName("first_name") + val first_name: String? = null, + @SerialName("last_name") + val last_name: String? = null, + @SerialName("avatar") + val avatar: String? = null, +) \ No newline at end of file diff --git a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieDetailFragment.kt b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieDetailFragment.kt index 338ddc7..7d741e2 100644 --- a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieDetailFragment.kt +++ b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieDetailFragment.kt @@ -10,6 +10,8 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import coil.load import coil.transform.RoundedCornersTransformation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking class MovieDetailFragment : Fragment(R.layout.fragment_movie_preview) { lateinit var poster: ImageView @@ -28,24 +30,26 @@ class MovieDetailFragment : Fragment(R.layout.fragment_movie_preview) { releaseDate = findViewById(R.id.releaseDate) } - val movieId = arguments?.getInt(ARG_ID) ?: 550 - val movie = //получаем фильм - poster.load("${BuildConfig.API_IMAGE_BASE_URL}${movie.posterPath}") { - transformations(RoundedCornersTransformation(16f)) - } - originalTitle.text = movie.originalTitle - overview.text = movie.overview - popularity.text = movie.popularity.toString() - releaseDate.text = movie.releaseDate + val userId = arguments?.getInt(ARG_ID) ?: 550 - activity?.onBackPressedDispatcher?.addCallback(this.viewLifecycleOwner, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - val manager: FragmentManager = parentFragmentManager - val transaction: FragmentTransaction = manager.beginTransaction() - transaction.replace(R.id.mainFragment, MovieListFragment()) - transaction.commit() + runBlocking (Dispatchers.IO){ + val user: User = Api().getUser(userId).data + poster.load("${user.avatar}") { + transformations(RoundedCornersTransformation(16f)) } - }) + originalTitle.text = user.first_name + } + + activity?.onBackPressedDispatcher?.addCallback( + this.viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val manager: FragmentManager = parentFragmentManager + val transaction: FragmentTransaction = manager.beginTransaction() + transaction.replace(R.id.mainFragment, MovieListFragment()) + transaction.commit() + } + }) } companion object { diff --git a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieItemAdapter.kt b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieItemAdapter.kt index 64177a5..e77d0d8 100644 --- a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieItemAdapter.kt +++ b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieItemAdapter.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.RoundedCornersTransformation -internal class MovieItemAdapter : ListAdapter(MOVIE_COMPARATOR) { +internal class MovieItemAdapter : ListAdapter(COMPARATOR) { private var clickListener: IOnItemClick? = null fun setClickListener(listener: IOnItemClick?) { @@ -37,7 +37,7 @@ internal class MovieItemAdapter : ListAdapter() { - override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean { + val COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } - override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean { + override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { return oldItem.id == newItem.id } - override fun getChangePayload(oldItem: Movie, newItem: Movie): Any? { + override fun getChangePayload(oldItem: User, newItem: User): Any? { return null } } diff --git a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieListFragment.kt b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieListFragment.kt index 762a176..17ebbfe 100644 --- a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieListFragment.kt +++ b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/MovieListFragment.kt @@ -7,16 +7,19 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking class MovieListFragment : Fragment(R.layout.fragment_movie_list) { lateinit var recycler: RecyclerView private val goToDetails = object : MovieItemAdapter.IOnItemClick { - override fun onItemClick(movie: Movie) { + override fun onItemClick(user: User) { val manager: FragmentManager = parentFragmentManager val transaction: FragmentTransaction = manager.beginTransaction() val detailsFragment = MovieDetailFragment() - detailsFragment.arguments = Bundle().apply { putInt(MovieDetailFragment.ARG_ID, movie.id) } + detailsFragment.arguments = + Bundle().apply { putInt(MovieDetailFragment.ARG_ID, user.id) } transaction.replace(R.id.mainFragment, detailsFragment) transaction.commit() } @@ -31,8 +34,12 @@ class MovieListFragment : Fragment(R.layout.fragment_movie_list) { } } val adapter = (recycler.adapter as MovieItemAdapter) - val movies = //получаем фильмы - adapter.submitList(movies) + + runBlocking(Dispatchers.IO) { + val users: Users = Api().getUsers() + adapter.submitList(users.data) + } + super.onViewCreated(view, savedInstanceState) } diff --git a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Network.kt b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Network.kt index af25f9f..859335d 100644 --- a/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Network.kt +++ b/HomeWorkNetwork/app/src/main/java/com/pg/homeworknetwork/Network.kt @@ -1,5 +1,40 @@ package com.pg.homeworknetwork +import io.ktor.client.* +import io.ktor.client.engine.android.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.features.logging.* +import io.ktor.client.request.* + class Api { + companion object Config { + private const val BASE_URL = BuildConfig.API_BASE_URL + private const val USERS = "/api/users" + private const val USER = "/api/users" + } + + private val client = HttpClient(Android) { + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + println(message) + } + } + level = LogLevel.ALL + } + + install(JsonFeature) { + serializer = KotlinxSerializer(kotlinx.serialization.json.Json { + ignoreUnknownKeys = true + }) + } + } + + // https://reqres.in/api/users + suspend fun getUsers(): Users = client.get(host = BASE_URL, path = USERS) + // https://reqres.in/api/users/2 + suspend fun getUser(userId: Int): Data = + client.get(host = BASE_URL, path = "$USER/$userId") } \ No newline at end of file diff --git a/HomeWorkNetwork/build.gradle b/HomeWorkNetwork/build.gradle index 0ccd671..3605be2 100644 --- a/HomeWorkNetwork/build.gradle +++ b/HomeWorkNetwork/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.1.2' apply false - id 'com.android.library' version '7.1.2' apply false + id 'com.android.application' version '7.1.3' apply false + id 'com.android.library' version '7.1.3' apply false id 'org.jetbrains.kotlin.android' version '1.6.10' apply false }