diff --git a/7weeks/.gitignore b/7weeks/.gitignore new file mode 100644 index 0000000..efaac43 --- /dev/null +++ b/7weeks/.gitignore @@ -0,0 +1,75 @@ +# OS files +.DS_Store + +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ diff --git a/7weeks/README.md b/7weeks/README.md new file mode 100644 index 0000000..12d5885 --- /dev/null +++ b/7weeks/README.md @@ -0,0 +1,157 @@ +## ViewGroup + +- Android의 모든 위젯은 View를 상속하여 구현하고 있다. + + ![View hierarchy](https://o7planning.org/en/10423/cache/images/i/1189616.png) + +### 주요 ViewGroup 종류와 사용법 + +#### LinearLayout + +- 명칭에서 알 수 있듯이 선형 모양의 레이아웃 +- `orientation` 이라는 필수 속성이 필요하며 지정하지 않을 경우 `horizontal` 이 기본 + +```xml + + + +``` + +#### RelativeLayout + +- 부모 또는 특정 View를 기준으로 특정 View의 상대 위치를 지정할 수 있는 레이아웃 + +```xml + + + + + + + +``` + +#### FrameLayout + +- 하나의 View 위젯을 표현하기 위한 레이아웃 +- 단, 레이아웃 하위에 여러 View 위젯을 추가할 순 있다. + +```xml + + + + + +``` + +#### ConstraintLayout + +- 뷰와 뷰 사이에 제약조건을 설정하여 위젯을 배치하기 위한 레이아웃 +- 속성이 엄청나게 많습니다. ~~(알아서 찾아보세요...)~~ + +```xml + + + + + +``` + +### 주요 Widget 종류와 사용법 + +#### RecyclerView + +- 기존의 ListView의 단점과 성능을 개선하여 제공되는 위젯 +- ViewHolder 패턴을 강제하여 아이템 View의 재사용성을 적극 활용한다. + +```xml + +``` + +```kotlin +fun setupRecyclerView() { + ExampleAdapter adapter = new ExampleAdapter() + LinearLayoutManager layoutManager = new LayoutManager(this) + with(exampleList) { + this.layoutManager = layoutManager + this.adapter = adpater + } +} +``` + +```kotlin +class ExampleAdapter : RecyclerView.Adapter() { + private var dataSet = mutableListOf() + + fun addItems(items: List) { + dataSet.addAll(items) + notifyDataSetChanged() + } + + fun updateItems(items: List) { + dataSet = items as MutableList + notifyDataSetChanged() + } + + class ExampleHolder(val containerView: View) : RecyclerView.ViewHolder(view) { + // other code + } + + + abstract class LayoutContainerViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleHolder { + return ExampleHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_example, parent, false) + ) + } + + override fun onBindViewHolder(holder: ExampleHolder, position: Int) { + // otehr code... + } + + override fun getItemCount(): Int = dataSet.size +} +``` + +#### ViewPager + +- 스와이프 액션을 통해 화면을 이동하기 위한 위젯 +- ~~샘플 코드 귀찮아요...~~ + +```xml + +``` + diff --git a/7weeks/app/.gitignore b/7weeks/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/7weeks/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/7weeks/app/build.gradle b/7weeks/app/build.gradle new file mode 100644 index 0000000..b95d92c --- /dev/null +++ b/7weeks/app/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 28 + buildToolsVersion "29.0.0" + defaultConfig { + applicationId "io.cro.example" + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + implementation 'androidx.core:core-ktx:1.0.2' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + implementation 'com.squareup.retrofit2:retrofit:2.6.1' + implementation 'com.squareup.retrofit2:converter-gson:2.6.1' + + implementation 'com.google.code.gson:gson:2.8.5' + + implementation 'com.jakewharton.timber:timber:4.7.1' + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/7weeks/app/proguard-rules.pro b/7weeks/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/7weeks/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/7weeks/app/src/androidTest/java/io/cro/example/ExampleInstrumentedTest.kt b/7weeks/app/src/androidTest/java/io/cro/example/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..7bd42e9 --- /dev/null +++ b/7weeks/app/src/androidTest/java/io/cro/example/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package io.cro.example + +import androidx.test.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("io.cro.example", appContext.packageName) + } +} diff --git a/7weeks/app/src/main/AndroidManifest.xml b/7weeks/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..eacd824 --- /dev/null +++ b/7weeks/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/7weeks/app/src/main/java/io/cro/example/ExampleApplication.kt b/7weeks/app/src/main/java/io/cro/example/ExampleApplication.kt new file mode 100644 index 0000000..b9b0ceb --- /dev/null +++ b/7weeks/app/src/main/java/io/cro/example/ExampleApplication.kt @@ -0,0 +1,11 @@ +package io.cro.example + +import android.app.Application +import timber.log.Timber + +class ExampleApplication : Application() { + override fun onCreate() { + super.onCreate() + Timber.plant(Timber.DebugTree()) + } +} \ No newline at end of file diff --git a/7weeks/app/src/main/java/io/cro/example/LegacyActivity.kt b/7weeks/app/src/main/java/io/cro/example/LegacyActivity.kt new file mode 100644 index 0000000..212e907 --- /dev/null +++ b/7weeks/app/src/main/java/io/cro/example/LegacyActivity.kt @@ -0,0 +1,69 @@ +package io.cro.example + +import android.os.AsyncTask +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import io.cro.example.adapter.UserAdapter +import kotlinx.android.synthetic.main.activity_legacy.* +import timber.log.Timber +import java.net.HttpURLConnection +import java.net.URL + +class LegacyActivity : AppCompatActivity() { + private val adapter: UserAdapter by lazy { UserAdapter() } + private val layoutManager: LinearLayoutManager by lazy { LinearLayoutManager(this) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_legacy) + setupRecyclerView() + } + + private fun setupRecyclerView() { + with(userList) { + adapter = this@LegacyActivity.adapter + layoutManager = this@LegacyActivity.layoutManager + } + + NetworkTask(adapter).execute("https://api.github.com/users") + } +} + +class NetworkTask(private val adapter: UserAdapter) : AsyncTask>() { + override fun doInBackground(vararg params: String?): List? { + val url = URL(params[0]) + var users: List? = null + + with(url.openConnection() as HttpURLConnection) { + requestMethod = "GET" + setRequestProperty("Accept", "application/vnd.github.v3+json") + + inputStream.bufferedReader().use { + val stringBuffer = StringBuffer() + var inputLine = it.readLine() + while (!inputLine.isNullOrEmpty()) { + stringBuffer.append(inputLine) + inputLine = it.readLine() + } + + users = Gson().fromJson( + stringBuffer.toString(), + object : TypeToken>() {}.type + ) + } + } + + return users + } + + override fun onPostExecute(result: List?) { + super.onPostExecute(result) + Timber.d("LEGACY::USERS::$result") + result?.let { + adapter.updateItem(it) + } + } +} diff --git a/7weeks/app/src/main/java/io/cro/example/MainActivity.kt b/7weeks/app/src/main/java/io/cro/example/MainActivity.kt new file mode 100644 index 0000000..3e72a46 --- /dev/null +++ b/7weeks/app/src/main/java/io/cro/example/MainActivity.kt @@ -0,0 +1,26 @@ +package io.cro.example + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import kotlinx.android.synthetic.main.activity_main.* + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + legacyButton.setOnClickListener { + Intent(this, LegacyActivity::class.java).apply { + startActivity(this) + } + } + + retrofitButton.setOnClickListener { + Intent(this, RetrofitActivity::class.java).apply { + startActivity(this) + } + } + } +} diff --git a/7weeks/app/src/main/java/io/cro/example/RetrofitActivity.kt b/7weeks/app/src/main/java/io/cro/example/RetrofitActivity.kt new file mode 100644 index 0000000..6130578 --- /dev/null +++ b/7weeks/app/src/main/java/io/cro/example/RetrofitActivity.kt @@ -0,0 +1,92 @@ +package io.cro.example + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import io.cro.example.adapter.UserAdapter +import kotlinx.android.synthetic.main.activity_legacy.* +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import timber.log.Timber +import java.io.IOException + +class RetrofitActivity : AppCompatActivity() { + private val retrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl("https://api.github.com/") + .client( + OkHttpClient.Builder() + .addInterceptor(HttpHeaderInterceptor()) + .build() + ) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + private val adapter: UserAdapter by lazy { UserAdapter() } + private val layoutManager: LinearLayoutManager by lazy { LinearLayoutManager(this) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_retrofit) + setupRecyclerView() + } + + private fun setupRecyclerView() { + with(userList) { + adapter = this@RetrofitActivity.adapter + layoutManager = this@RetrofitActivity.layoutManager + } + + getUsers() + } + + private fun getUsers() { + retrofit.create(GitHubApi::class.java) + .getUsers().enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: retrofit2.Response> + ) { + if (response.isSuccessful) { + response.body()?.let { + adapter.updateItem(it) + } + } + } + + override fun onFailure(call: Call>, t: Throwable) { + Timber.e("RETROFIT::onFailure::${t.message}") + } + }) + } +} + +class HttpHeaderInterceptor: Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val requestBuilder = request.newBuilder().apply { + addHeader("Accept", "application/vnd.github.v3+json") + } + + return chain.proceed(requestBuilder.build()) + } + + companion object { + private enum class Headers(val key: String, val value: String) { + ACCEPT("Accept", "application/vnd.github.v3+json"); + } + } +} + +interface GitHubApi { + @GET("/users") + fun getUsers(): Call> +} diff --git a/7weeks/app/src/main/java/io/cro/example/UserProfile.kt b/7weeks/app/src/main/java/io/cro/example/UserProfile.kt new file mode 100644 index 0000000..4b08be0 --- /dev/null +++ b/7weeks/app/src/main/java/io/cro/example/UserProfile.kt @@ -0,0 +1,68 @@ +package io.cro.example + +import com.google.gson.annotations.SerializedName + +data class UserProfile( + @SerializedName("login") + val login: String = "", + @SerializedName("id") + val id: Int = 0, + @SerializedName("node_id") + val nodeId: String = "", + @SerializedName("avatar_url") + val avatarUrl: String = "", + @SerializedName("gravatar_id") + val gravatarId: String = "", + @SerializedName("url") + val url: String = "", + @SerializedName("html_url") + val htmlUrl: String = "", + @SerializedName("followers_url") + val followersUrl: String = "", + @SerializedName("following_url") + val followingUrl: String = "", + @SerializedName("gists_url") + val gistsUrl: String = "", + @SerializedName("starred_url") + val starredUrl: String = "", + @SerializedName("subscriptions_url") + val subscriptionsUrl: String = "", + @SerializedName("organizations_url") + val organizationsUrl: String = "", + @SerializedName("repos_url") + val reposUrl: String = "", + @SerializedName("events_url") + val eventsUrl: String = "", + @SerializedName("received_events_url") + val receivedEventsUrl: String = "", + @SerializedName("type") + val type: String = "", + @SerializedName("site_admin") + val siteAdmin: Boolean = false, + @SerializedName("name") + val name: String = "", + @SerializedName("company") + val company: String = "", + @SerializedName("blog") + val blog: String = "", + @SerializedName("location") + val location: String = "", + @SerializedName("email") + val email: String = "", + @SerializedName("hireable") + val hireable: Boolean = false, + @SerializedName("bio") + val bio: String = "", + @SerializedName("public_repos") + val publicRepos: Int = 0, + @SerializedName("public_gists") + val publicGists: Int = 0, + @SerializedName("followers") + val followers: Int = 0, + @SerializedName("following") + val following: Int = 0, + @SerializedName("created_at") + val createdAt: String = "", + @SerializedName("updated_at") + val updatedAt: String = "" +) \ No newline at end of file diff --git a/7weeks/app/src/main/java/io/cro/example/adapter/UserAdapter.kt b/7weeks/app/src/main/java/io/cro/example/adapter/UserAdapter.kt new file mode 100644 index 0000000..d0906f5 --- /dev/null +++ b/7weeks/app/src/main/java/io/cro/example/adapter/UserAdapter.kt @@ -0,0 +1,34 @@ +package io.cro.example.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.cro.example.R +import io.cro.example.UserProfile +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_user.view.* + +class UserAdapter : RecyclerView.Adapter() { + private var dataSet = mutableListOf() + + fun updateItem(dataSet: List) { + this.dataSet = dataSet as MutableList + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder = + UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false)) + + override fun onBindViewHolder(holder: UserViewHolder, position: Int) { + holder.bindTo(dataSet[position]) + } + + override fun getItemCount(): Int = dataSet.size +} + +class UserViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer { + fun bindTo(data: UserProfile) { + itemView.nameTextView.text = data.login + } +} \ No newline at end of file diff --git a/7weeks/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/7weeks/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/7weeks/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/7weeks/app/src/main/res/drawable/ic_launcher_background.xml b/7weeks/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/7weeks/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/7weeks/app/src/main/res/layout/activity_legacy.xml b/7weeks/app/src/main/res/layout/activity_legacy.xml new file mode 100644 index 0000000..ba00dbd --- /dev/null +++ b/7weeks/app/src/main/res/layout/activity_legacy.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/7weeks/app/src/main/res/layout/activity_main.xml b/7weeks/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b51028c --- /dev/null +++ b/7weeks/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,32 @@ + + + + + +