From aabadfac3c28a13fd334ccfb9085dbf269e22507 Mon Sep 17 00:00:00 2001 From: Satoru Sasaki Date: Fri, 24 Feb 2023 10:41:27 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=E5=8B=95=E4=BD=9C=E7=A2=BA=E8=AA=8D?= =?UTF-8?q?=E7=94=A8=E5=B7=AE=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.json | 46 ++++ app/src/main/AndroidManifest.xml | 16 +- .../us/mitene/practicalexam/MainActivity.kt | 223 +++++++++++++++++- app/src/main/res/layout/activity_main.xml | 42 ++-- .../main/res/layout/list_item_repository.xml | 42 ++++ 5 files changed, 346 insertions(+), 23 deletions(-) create mode 100644 app/schemas/us.mitene.practicalexam.AppDatabase/1.json create mode 100644 app/src/main/res/layout/list_item_repository.xml diff --git a/app/schemas/us.mitene.practicalexam.AppDatabase/1.json b/app/schemas/us.mitene.practicalexam.AppDatabase/1.json new file mode 100644 index 0000000..8868c91 --- /dev/null +++ b/app/schemas/us.mitene.practicalexam.AppDatabase/1.json @@ -0,0 +1,46 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "8e178f0e3f23a65aa89c3591f4853190", + "entities": [ + { + "tableName": "RepositoryResponse", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8e178f0e3f23a65aa89c3591f4853190')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a107b8..26f5ff1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,15 +2,15 @@ + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/Theme.PracticalExam"> + android:name=".MainActivity" + android:exported="true"> diff --git a/app/src/main/java/us/mitene/practicalexam/MainActivity.kt b/app/src/main/java/us/mitene/practicalexam/MainActivity.kt index 903cdae..e30afb5 100644 --- a/app/src/main/java/us/mitene/practicalexam/MainActivity.kt +++ b/app/src/main/java/us/mitene/practicalexam/MainActivity.kt @@ -1,11 +1,232 @@ package us.mitene.practicalexam +import android.content.Context import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.room.* +import androidx.room.Database +import com.google.gson.GsonBuilder +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import timber.log.Timber +import us.mitene.practicalexam.databinding.ActivityMainBinding +import us.mitene.practicalexam.databinding.ListItemRepositoryBinding +import java.lang.Exception +import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { + lateinit var binding: ActivityMainBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + + // setup timber + Timber.plant(Timber.DebugTree()) + + val adapter = RepositoryAdapter() + + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(this) + + // fetch + lifecycleScope.launchWhenCreated { + val entities = MainRepository(this@MainActivity).getOrganizationRepositories() + adapter.update(entities) + } } +} + +class MainRepository(context: Context) { + private val remote = Service.githubService + private val local = DatabaseProvider.githubDao(context) + + suspend fun getOrganizationRepositories(): List { + return withContext(Dispatchers.IO) { + try { + local.getAll().ifEmpty { + // remote + val remoteData = remote.organization("mixi-inc") + // save entity + local.upsert(*remoteData.toTypedArray()) + remoteData + }.map { + RepositoryMapper.toEntity(it) + } + } catch (e: Exception) { + Timber.w(e) + emptyList() + } + } + } +} + +// api client (retrofit, okhttp) +object Service { + private val client: OkHttpClient = createOkhttpClient() + val githubService: GithubService = createGithubService() + + private fun createOkhttpClient(): OkHttpClient { + // create http client + val httpClient = OkHttpClient.Builder() + .addInterceptor(Interceptor { chain -> + val original = chain.request() + + //header + val request = original.newBuilder() + .header("Accept", "vnd.github.v3+json") + .method(original.method, original.body) + .build() + + return@Interceptor chain.proceed(request) + }) + .readTimeout(30, TimeUnit.SECONDS) + + // log interceptor + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + httpClient.addInterceptor(loggingInterceptor) + + return httpClient.build() + } + + @OptIn(ExperimentalSerializationApi::class) + private fun createGithubService(useGson: Boolean = false): GithubService { + val converter = if (useGson) { + val gson = GsonBuilder().serializeNulls().create() + GsonConverterFactory.create(gson) + } else { + val formatter = Json { ignoreUnknownKeys = true } + formatter.asConverterFactory("application/json".toMediaType()) + } + + // create retrofit + val builder = Retrofit.Builder() + .baseUrl("https://api.github.com") + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(converter) + .client(client) + + return builder.build().create(GithubService::class.java) + } +} + +// entity +@Serializable +@Entity +data class RepositoryResponse( + @PrimaryKey + val id: Int, + val name: String, + val url: String, +) + +data class RepositoryEntity( + val name: String, + val url: String, +) + +object RepositoryMapper { + fun toEntity(response: RepositoryResponse) = RepositoryEntity( + name = response.name, + url = response.url, + ) +} + +// room +@Dao +interface GithubDao { + @Query("SELECT * FROM RepositoryResponse") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg repository: RepositoryResponse) + + @Delete + suspend fun delete(vararg repository: RepositoryResponse) +} + +@Database( + entities = [ + RepositoryResponse::class + ], + version = 1, + exportSchema = true +) +abstract class AppDatabase : RoomDatabase() { + abstract fun githubDao(): GithubDao +} + +object DatabaseProvider { + private var db: AppDatabase? = null + + private fun db(context: Context): AppDatabase { + if (db != null) return db!! + db = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "app-database" + ).build() + return db!! + } + + fun githubDao(context: Context) = db(context).githubDao() +} + +// adapter +class RepositoryAdapter : RecyclerView.Adapter() { + private var repositories = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = ListItemRepositoryBinding.inflate(inflater, parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(repositories[position]) + } + + override fun getItemCount(): Int = repositories.size + + fun update(data: List) { + repositories = data + notifyItemRangeChanged(0, repositories.size) + } + + class ViewHolder( + private val binding: ListItemRepositoryBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: RepositoryEntity) { + binding.name.text = item.name + binding.url.text = item.url + binding.root.setOnClickListener { + Timber.d("TESTTEST ${item.name}") + } + } + } +} + +interface GithubService { + @GET("/orgs/{org}/repos") + suspend fun organization(@Path("org") org: String): List } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c08b7a3..c0f9d5a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,18 +1,32 @@ - + xmlns:app="http://schemas.android.com/apk/res-auto"> - + - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_repository.xml b/app/src/main/res/layout/list_item_repository.xml new file mode 100644 index 0000000..a8cd315 --- /dev/null +++ b/app/src/main/res/layout/list_item_repository.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file From 8cef898079ed9b228f372968e1dd54c49129e57f Mon Sep 17 00:00:00 2001 From: Satoru Sasaki Date: Fri, 24 Feb 2023 13:27:51 +0900 Subject: [PATCH 02/11] =?UTF-8?q?internet=20permision=20=E5=BF=98=E3=82=8C?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 26f5ff1..c09e34a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + Date: Fri, 28 Apr 2023 18:29:16 +0900 Subject: [PATCH 03/11] =?UTF-8?q?data=20layer=20=E5=88=86=E3=81=91?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.json | 6 +- .../practicalexam/data/GithubRepoEntity.kt | 30 ++++ .../practicalexam/data/GithubRepository.kt | 153 ++++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) rename app/schemas/{us.mitene.practicalexam.AppDatabase => us.mitene.practicalexam.data.AppDatabase}/1.json (88%) create mode 100644 app/src/main/java/us/mitene/practicalexam/data/GithubRepoEntity.kt create mode 100644 app/src/main/java/us/mitene/practicalexam/data/GithubRepository.kt diff --git a/app/schemas/us.mitene.practicalexam.AppDatabase/1.json b/app/schemas/us.mitene.practicalexam.data.AppDatabase/1.json similarity index 88% rename from app/schemas/us.mitene.practicalexam.AppDatabase/1.json rename to app/schemas/us.mitene.practicalexam.data.AppDatabase/1.json index 8868c91..2be6bc7 100644 --- a/app/schemas/us.mitene.practicalexam.AppDatabase/1.json +++ b/app/schemas/us.mitene.practicalexam.data.AppDatabase/1.json @@ -2,10 +2,10 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "8e178f0e3f23a65aa89c3591f4853190", + "identityHash": "6e1226bef5a0160d9fa88e8d46607005", "entities": [ { - "tableName": "RepositoryResponse", + "tableName": "GithubRepoResponse", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { @@ -40,7 +40,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8e178f0e3f23a65aa89c3591f4853190')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e1226bef5a0160d9fa88e8d46607005')" ] } } \ No newline at end of file diff --git a/app/src/main/java/us/mitene/practicalexam/data/GithubRepoEntity.kt b/app/src/main/java/us/mitene/practicalexam/data/GithubRepoEntity.kt new file mode 100644 index 0000000..efd3cfc --- /dev/null +++ b/app/src/main/java/us/mitene/practicalexam/data/GithubRepoEntity.kt @@ -0,0 +1,30 @@ +package us.mitene.practicalexam.data + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +// response +@Serializable +@Entity +data class GithubRepoResponse( + @PrimaryKey + val id: Int, + val name: String, + val url: String, +) + +// entity +data class GithubRepoEntity( + val name: String, + val url: String, +) + +// mapper +object GithubRepoEntityMapper { + fun toEntity(response: GithubRepoResponse) = GithubRepoEntity( + name = response.name, + url = response.url, + ) +} + diff --git a/app/src/main/java/us/mitene/practicalexam/data/GithubRepository.kt b/app/src/main/java/us/mitene/practicalexam/data/GithubRepository.kt new file mode 100644 index 0000000..6a759bc --- /dev/null +++ b/app/src/main/java/us/mitene/practicalexam/data/GithubRepository.kt @@ -0,0 +1,153 @@ +package us.mitene.practicalexam.data + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import com.google.gson.GsonBuilder +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import timber.log.Timber +import java.util.concurrent.TimeUnit + +class GithubRepository( + context: Context, + private val remote: GithubService = Service.githubService, + private val local: GithubDao = DatabaseProvider.githubDao(context), +) { + + suspend fun getOrganizationRepositories(): List { + return withContext(Dispatchers.IO) { + try { + local.getAll().ifEmpty { + // remote + val remoteData = remote.organization("mixi-inc") + // save entity + local.upsert(*remoteData.toTypedArray()) + remoteData + }.map { + GithubRepoEntityMapper.toEntity(it) + } + } catch (e: Exception) { + Timber.w(e) + emptyList() + } + } + } +} + +/** + * remote + */ +// api +interface GithubService { + @GET("/orgs/{org}/repos") + suspend fun organization(@Path("org") org: String): List +} + +// client (retrofit, okhttp) +object Service { + private val client: OkHttpClient = createOkhttpClient() + val githubService: GithubService = createGithubService() + + private fun createOkhttpClient(): OkHttpClient { + // create http client + val httpClient = OkHttpClient.Builder() + .addInterceptor(Interceptor { chain -> + val original = chain.request() + + //header + val request = original.newBuilder() + .header("Accept", "vnd.github.v3+json") + .method(original.method, original.body) + .build() + + return@Interceptor chain.proceed(request) + }) + .readTimeout(30, TimeUnit.SECONDS) + + // log interceptor + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + httpClient.addInterceptor(loggingInterceptor) + + return httpClient.build() + } + + private fun createGithubService(useGson: Boolean = false): GithubService { + val converter = if (useGson) { + val gson = GsonBuilder().serializeNulls().create() + GsonConverterFactory.create(gson) + } else { + val formatter = Json { ignoreUnknownKeys = true } + formatter.asConverterFactory("application/json".toMediaType()) + } + + // create retrofit + val builder = Retrofit.Builder() + .baseUrl("https://api.github.com") + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(converter) + .client(client) + + return builder.build().create(GithubService::class.java) + } +} + +/** + * local + */ +@Dao +interface GithubDao { + @Query("SELECT * FROM GithubRepoResponse") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(vararg repository: GithubRepoResponse) + + @Delete + suspend fun delete(vararg repository: GithubRepoResponse) +} + +@Database( + entities = [ + GithubRepoResponse::class + ], + version = 1, + exportSchema = true +) +abstract class AppDatabase : RoomDatabase() { + abstract fun githubDao(): GithubDao +} + +object DatabaseProvider { + private var db: AppDatabase? = null + + private fun db(context: Context): AppDatabase { + if (db != null) return db!! + db = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "app-database" + ).build() + return db!! + } + + fun githubDao(context: Context) = db(context).githubDao() +} From 9bb98943a90805320adc915f406174a004d76b3e Mon Sep 17 00:00:00 2001 From: Satoru Sasaki Date: Fri, 28 Apr 2023 18:30:11 +0900 Subject: [PATCH 04/11] =?UTF-8?q?compose=20=E4=BD=BF=E3=82=8F=E3=81=AA?= =?UTF-8?q?=E3=81=84=20activity=20=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../us/mitene/practicalexam/MainActivity.kt | 229 +----------------- .../mitene/practicalexam/ui/NormalActivity.kt | 75 ++++++ app/src/main/res/layout/activity_main.xml | 47 ++-- app/src/main/res/layout/activity_normal.xml | 23 ++ 5 files changed, 128 insertions(+), 247 deletions(-) create mode 100644 app/src/main/java/us/mitene/practicalexam/ui/NormalActivity.kt create mode 100644 app/src/main/res/layout/activity_normal.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c09e34a..4bf19ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/app/src/main/java/us/mitene/practicalexam/MainActivity.kt b/app/src/main/java/us/mitene/practicalexam/MainActivity.kt index e30afb5..3ec1a33 100644 --- a/app/src/main/java/us/mitene/practicalexam/MainActivity.kt +++ b/app/src/main/java/us/mitene/practicalexam/MainActivity.kt @@ -1,232 +1,23 @@ package us.mitene.practicalexam -import android.content.Context -import androidx.appcompat.app.AppCompatActivity +import android.content.Intent import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.room.* -import androidx.room.Database -import com.google.gson.GsonBuilder -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.http.GET -import retrofit2.http.Path -import timber.log.Timber +import androidx.appcompat.app.AppCompatActivity import us.mitene.practicalexam.databinding.ActivityMainBinding -import us.mitene.practicalexam.databinding.ListItemRepositoryBinding -import java.lang.Exception -import java.util.concurrent.TimeUnit +import us.mitene.practicalexam.ui.ComposeActivity +import us.mitene.practicalexam.ui.NormalActivity class MainActivity : AppCompatActivity() { - lateinit var binding: ActivityMainBinding - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) - // setup timber - Timber.plant(Timber.DebugTree()) - - val adapter = RepositoryAdapter() - - binding = DataBindingUtil.setContentView(this, R.layout.activity_main) - binding.recyclerView.adapter = adapter - binding.recyclerView.layoutManager = LinearLayoutManager(this) - - // fetch - lifecycleScope.launchWhenCreated { - val entities = MainRepository(this@MainActivity).getOrganizationRepositories() - adapter.update(entities) + binding.normal.setOnClickListener { + startActivity(Intent(this, NormalActivity::class.java)) } - } -} - -class MainRepository(context: Context) { - private val remote = Service.githubService - private val local = DatabaseProvider.githubDao(context) - - suspend fun getOrganizationRepositories(): List { - return withContext(Dispatchers.IO) { - try { - local.getAll().ifEmpty { - // remote - val remoteData = remote.organization("mixi-inc") - // save entity - local.upsert(*remoteData.toTypedArray()) - remoteData - }.map { - RepositoryMapper.toEntity(it) - } - } catch (e: Exception) { - Timber.w(e) - emptyList() - } - } - } -} - -// api client (retrofit, okhttp) -object Service { - private val client: OkHttpClient = createOkhttpClient() - val githubService: GithubService = createGithubService() - - private fun createOkhttpClient(): OkHttpClient { - // create http client - val httpClient = OkHttpClient.Builder() - .addInterceptor(Interceptor { chain -> - val original = chain.request() - - //header - val request = original.newBuilder() - .header("Accept", "vnd.github.v3+json") - .method(original.method, original.body) - .build() - - return@Interceptor chain.proceed(request) - }) - .readTimeout(30, TimeUnit.SECONDS) - - // log interceptor - val loggingInterceptor = HttpLoggingInterceptor() - loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY - httpClient.addInterceptor(loggingInterceptor) - - return httpClient.build() - } - - @OptIn(ExperimentalSerializationApi::class) - private fun createGithubService(useGson: Boolean = false): GithubService { - val converter = if (useGson) { - val gson = GsonBuilder().serializeNulls().create() - GsonConverterFactory.create(gson) - } else { - val formatter = Json { ignoreUnknownKeys = true } - formatter.asConverterFactory("application/json".toMediaType()) + binding.compose.setOnClickListener { + startActivity(Intent(this, ComposeActivity::class.java)) } - - // create retrofit - val builder = Retrofit.Builder() - .baseUrl("https://api.github.com") - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .addConverterFactory(converter) - .client(client) - - return builder.build().create(GithubService::class.java) } } - -// entity -@Serializable -@Entity -data class RepositoryResponse( - @PrimaryKey - val id: Int, - val name: String, - val url: String, -) - -data class RepositoryEntity( - val name: String, - val url: String, -) - -object RepositoryMapper { - fun toEntity(response: RepositoryResponse) = RepositoryEntity( - name = response.name, - url = response.url, - ) -} - -// room -@Dao -interface GithubDao { - @Query("SELECT * FROM RepositoryResponse") - suspend fun getAll(): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg repository: RepositoryResponse) - - @Delete - suspend fun delete(vararg repository: RepositoryResponse) -} - -@Database( - entities = [ - RepositoryResponse::class - ], - version = 1, - exportSchema = true -) -abstract class AppDatabase : RoomDatabase() { - abstract fun githubDao(): GithubDao -} - -object DatabaseProvider { - private var db: AppDatabase? = null - - private fun db(context: Context): AppDatabase { - if (db != null) return db!! - db = Room.databaseBuilder( - context.applicationContext, - AppDatabase::class.java, - "app-database" - ).build() - return db!! - } - - fun githubDao(context: Context) = db(context).githubDao() -} - -// adapter -class RepositoryAdapter : RecyclerView.Adapter() { - private var repositories = emptyList() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = ListItemRepositoryBinding.inflate(inflater, parent, false) - return ViewHolder(binding) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(repositories[position]) - } - - override fun getItemCount(): Int = repositories.size - - fun update(data: List) { - repositories = data - notifyItemRangeChanged(0, repositories.size) - } - - class ViewHolder( - private val binding: ListItemRepositoryBinding - ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: RepositoryEntity) { - binding.name.text = item.name - binding.url.text = item.url - binding.root.setOnClickListener { - Timber.d("TESTTEST ${item.name}") - } - } - } -} - -interface GithubService { - @GET("/orgs/{org}/repos") - suspend fun organization(@Path("org") org: String): List -} \ No newline at end of file diff --git a/app/src/main/java/us/mitene/practicalexam/ui/NormalActivity.kt b/app/src/main/java/us/mitene/practicalexam/ui/NormalActivity.kt new file mode 100644 index 0000000..a978461 --- /dev/null +++ b/app/src/main/java/us/mitene/practicalexam/ui/NormalActivity.kt @@ -0,0 +1,75 @@ +package us.mitene.practicalexam.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch +import timber.log.Timber +import us.mitene.practicalexam.R +import us.mitene.practicalexam.data.GithubRepoEntity +import us.mitene.practicalexam.data.GithubRepository +import us.mitene.practicalexam.databinding.ActivityNormalBinding +import us.mitene.practicalexam.databinding.ListItemRepositoryBinding + +class NormalActivity : AppCompatActivity() { + lateinit var binding: ActivityNormalBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val adapter = GithubRepoAdapter() + + binding = DataBindingUtil.setContentView(this, R.layout.activity_normal) + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(this) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + // fetch data + val entities = GithubRepository(this@NormalActivity).getOrganizationRepositories() + adapter.update(entities) + } + } + } +} + +// adapter +class GithubRepoAdapter : RecyclerView.Adapter() { + private var repositories = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = ListItemRepositoryBinding.inflate(inflater, parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(repositories[position]) + } + + override fun getItemCount(): Int = repositories.size + + fun update(data: List) { + repositories = data + notifyItemRangeChanged(0, repositories.size) + } + + class ViewHolder( + private val binding: ListItemRepositoryBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GithubRepoEntity) { + binding.name.text = item.name + binding.url.text = item.url + binding.root.setOnClickListener { + Timber.d("TESTTEST ${item.name}") + } + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c0f9d5a..02dbe6c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,32 +1,23 @@ - + - +