Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
aabadfa
動作確認用差分
caad1229 Feb 24, 2023
8cef898
internet permision 忘れた
caad1229 Feb 24, 2023
a520dfd
Merge remote-tracking branch 'origin/update-library-20230224' into ch…
caad1229 Feb 24, 2023
c681f82
Merge remote-tracking branch 'origin/master' into check-update
caad1229 Apr 28, 2023
6ffc544
Merge branch 'update-library-20230428-with-enable-compose' into check…
caad1229 Apr 28, 2023
d62cc31
Merge branch 'update-library-20230428-with-enable-compose' into check…
caad1229 Apr 28, 2023
84ac2ba
data layer 分ける
caad1229 Apr 28, 2023
9bb9894
compose 使わない activity を追加
caad1229 Apr 28, 2023
c5e062a
忘れ
caad1229 Apr 28, 2023
a963ae3
compose 用 activity 表示
caad1229 Apr 28, 2023
4041546
Merge remote-tracking branch 'origin/update-library-20230428-with-ena…
caad1229 Apr 28, 2023
3099016
fix bug
caad1229 Apr 28, 2023
cc76593
Merge remote-tracking branch 'origin/update-library-20230428-with-ena…
caad1229 May 2, 2023
9446afc
fix
caad1229 May 2, 2023
0c0571a
Merge branch 'update-library-20230502-agp' into check-update
caad1229 May 2, 2023
4070cf3
Merge branch 'update-library-20231010' into check-update
caad1229 Oct 10, 2023
6e4a3cd
version 指定
caad1229 Oct 10, 2023
01a6b66
Merge remote-tracking branch 'origin/main' into check-update
caad1229 Oct 10, 2023
9d4c805
Merge branch 'update-library-20240110' into check-update
caad1229 Jan 10, 2024
28f0812
Merge remote-tracking branch 'origin/main' into check-update
caad1229 Jan 10, 2024
54d078d
Merge branch 'update-library-20240321' into check-update
caad1229 Mar 21, 2024
d8e0806
Merge remote-tracking branch 'origin/main' into check-update
caad1229 Mar 22, 2024
683d82f
Merge branch 'update-library-20240527' into check-update
caad1229 May 27, 2024
fdf5396
Merge remote-tracking branch 'origin/update-library-20240920' into ch…
caad1229 Sep 25, 2024
1c1fbcd
マージミス修正
caad1229 Sep 25, 2024
7e5dd82
fix build error
caad1229 Sep 25, 2024
b2a7c86
Merge remote-tracking branch 'origin/main' into check-update
caad1229 Sep 25, 2024
580384a
Merge branch 'update-library-20241108' into check-update
caad1229 Nov 8, 2024
bb84ac6
Merge remote-tracking branch 'origin/main' into check-update
caad1229 Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/schemas/us.mitene.practicalexam.data.AppDatabase/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "6e1226bef5a0160d9fa88e8d46607005",
"entities": [
{
"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": [
{
"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, '6e1226bef5a0160d9fa88e8d46607005')"
]
}
}
20 changes: 12 additions & 8 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />

<application
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: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">
<activity
android:name=".MainActivity"
android:exported="true">
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.NormalActivity" />
<activity android:name=".ui.ComposeActivity" />
</application>

</manifest>
18 changes: 15 additions & 3 deletions app/src/main/java/us/mitene/practicalexam/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
package us.mitene.practicalexam

import androidx.appcompat.app.AppCompatActivity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import us.mitene.practicalexam.databinding.ActivityMainBinding
import us.mitene.practicalexam.ui.ComposeActivity
import us.mitene.practicalexam.ui.NormalActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.normal.setOnClickListener {
startActivity(Intent(this, NormalActivity::class.java))
}
binding.compose.setOnClickListener {
startActivity(Intent(this, ComposeActivity::class.java))
}
}
}
}
30 changes: 30 additions & 0 deletions app/src/main/java/us/mitene/practicalexam/data/GithubRepoEntity.kt
Original file line number Diff line number Diff line change
@@ -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,
)
}

155 changes: 155 additions & 0 deletions app/src/main/java/us/mitene/practicalexam/data/GithubRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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 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.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.kotlinx.serialization.asConverterFactory
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(organization: String): List<GithubRepoEntity> {
return withContext(Dispatchers.IO) {
try {
local.getAll().ifEmpty {
// remote
val remoteData = remote.organization(organization)
// 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<GithubRepoResponse>
}

// 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
// ref : https://docs.github.com/ja/free-pro-team@latest/rest/repos/repos?apiVersion=2022-11-28#list-organization-repositories
val request = original.newBuilder()
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.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(RxJava3CallAdapterFactory.create())
.addConverterFactory(converter)
.client(client)

return builder.build().create(GithubService::class.java)
}
}

/**
* local
*/
@Dao
interface GithubDao {
@Query("SELECT * FROM GithubRepoResponse")
suspend fun getAll(): List<GithubRepoResponse>

@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()
}
63 changes: 63 additions & 0 deletions app/src/main/java/us/mitene/practicalexam/ui/ComposeActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package us.mitene.practicalexam.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import us.mitene.practicalexam.data.GithubRepoEntity
import us.mitene.practicalexam.data.GithubRepository
import us.mitene.practicalexam.ui.theme.PracticalExamTheme

class ComposeActivity : ComponentActivity() {
private val repository by lazy { GithubRepository(this) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PracticalExamTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
GithubRepoListScreen(repository = repository)
}
}
}
}
}

@Suppress("UNCHECKED_CAST")
class ComposeViewModelFactory(
private val repository: GithubRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ComposeViewModel(repository) as T
}
}

class ComposeViewModel(
private val repository: GithubRepository
) : ViewModel() {
var uiState by mutableStateOf(ComposeUiState())

fun fetchRepos(organization: String = "mixi-inc") {
viewModelScope.launch {
uiState = uiState.copy(repos = repository.getOrganizationRepositories(organization))
}
}
}

data class ComposeUiState(
val repos: List<GithubRepoEntity> = emptyList()
)
Loading