Skip to content

Commit

Permalink
Merge branch 'release/1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
LNMCode committed May 2, 2022
2 parents 8bb6b69 + a92d86d commit 600c98d
Show file tree
Hide file tree
Showing 188 changed files with 5,209 additions and 44 deletions.
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
This is gallery app
<h1 align="center">Gallery App</h1></br>
<p align="center">
GalleryApp is application show image from Unsplash API, based on MVVM architecture.
</p>

# GalleryApp
<p align="center">

</p>

## Download
Go to the [Releases](https://github.com/LNMCode/GalleryApp/releases) to download the latest APK.

## UI Application

[UI Application](https://www.figma.com/file/abtgGeg11LmHEAgyWqTfTg/Art-gallery-app-UI-(Community)?node-id=0%3A1) - UI of application based on ui shared in Figma.

## Screenshots
<p align="center">
<img src="/preview/preview01.gif" width="32%"/>
<img src="/preview/preview02.gif" width="32%"/>
<img src="/preview/preview03.gif" width="32%"/>
</p>

## Tech stack & Open-source libraries
- Minimum SDK level 21
- 100% [Kotlin](https://kotlinlang.org/) based + [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) + [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) for asynchronous.
- JetPack
- [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle) - perform action when lifecycle state changes.
- [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - store and manage UI-related data in a lifecycle conscious way.
- [Room](https://developer.android.com/topic/libraries/architecture/room) - a persistence library provides an abstraction layer over SQLite.
- Architecture
- MVVM Architecture (View - DataBinding - ViewModel - Model)
- Repository pattern
- [Koin](https://github.com/InsertKoinIO/koin) - dependency injection
- Material Design & Animations
- [Retrofit2 & Gson](https://github.com/square/retrofit) - constructing the REST API
- [OkHttp3](https://github.com/square/okhttp) - implementing interceptor, logging and mocking web server
- [Glide](https://github.com/bumptech/glide) - loading images
- [Timber](https://github.com/JakeWharton/timber) - logging
- Shared element container transform/transition between fragments
Empty file added app/README.md
Empty file.
Empty file added app/README.rm
Empty file.
15 changes: 12 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.6.21"

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
Expand All @@ -55,6 +58,7 @@ dependencies {
def retrofit2_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"

// -- Room
def room_version = "2.4.2"
Expand All @@ -67,13 +71,13 @@ dependencies {
def nav_version = "2.4.2"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-runtime:$nav_version"
implementation "androidx.navigation:navigation-runtime-ktx:$nav_version"

def material_version = "1.5.0"
implementation "com.google.android.material:material:$material_version"

//glide
def glide_version = "4.12.0"
def glide_version = "4.13.0"
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"

Expand All @@ -91,6 +95,11 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.2'

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
// architecture components
def lifecycle_version = "2.4.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

// debugging
implementation "com.jakewharton.timber:timber:5.0.1"
}
6 changes: 4 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lnmcode.galleryapp">

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

<application
android:name=".presentation.BaseApplication"
android:name=".presentation.ui.BaseApplication"
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.GalleryApp">
<activity
android:name=".presentation.MainActivity"
android:name=".presentation.ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.lnmcode.galleryapp.bindables

import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingComponent
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding

abstract class BindingActivity<T : ViewDataBinding> constructor(
@LayoutRes private val contentLayoutId: Int
) : AppCompatActivity() {

/** This interface is generated during compilation to contain getters for all used instance `BindingAdapters`. */
protected var bindingComponent: DataBindingComponent? = DataBindingUtil.getDefaultComponent()

/**
* A data-binding property will be initialized before being called [onCreate].
* And inflates using the [contentLayoutId] as a content view for activities.
*/
@BindingOnly
protected val binding: T by lazy(LazyThreadSafetyMode.NONE) {
DataBindingUtil.setContentView(this, contentLayoutId, bindingComponent)
}

/**
* An executable inline binding function that receives a binding receiver in lambda.
*
* @param block A lambda block will be executed with the binding receiver.
* @return T A generic class that extends [ViewDataBinding] and generated by DataBinding on compile time.
*/
@BindingOnly
protected inline fun binding(block: T.() -> Unit): T {
return binding.apply(block)
}

/**
* Ensures the [binding] property should be executed before being called [onCreate].
*/
init {
addOnContextAvailableListener {
binding.notifyChange()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.lnmcode.galleryapp.bindables

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty

/**
*
* A binding extension for inflating a [layoutRes] and returns a DataBinding type [T].
*
* @param layoutRes The layout resource ID of the layout to inflate.
* @param attachToParent Whether the inflated hierarchy should be attached to the parent parameter.
*
* @return T A DataBinding class that inflated using the [layoutRes].
*/
@BindingOnly
fun <T : ViewDataBinding> ViewGroup.binding(
@LayoutRes layoutRes: Int,
attachToParent: Boolean = false
): T {
return DataBindingUtil.inflate(
LayoutInflater.from(context), layoutRes, this, attachToParent
)
}

/**
*
* A binding extension for inflating a [layoutRes] and returns a DataBinding type [T] with a receiver.
*
* @param layoutRes The layout resource ID of the layout to inflate.
* @param attachToParent Whether the inflated hierarchy should be attached to the parent parameter.
* @param block A DataBinding receiver lambda.
*
* @return T A DataBinding class that inflated using the [layoutRes].
*/
@BindingOnly
fun <T : ViewDataBinding> ViewGroup.binding(
@LayoutRes layoutRes: Int,
attachToParent: Boolean = false,
block: T.() -> Unit
): T {
return binding<T>(layoutRes, attachToParent).apply(block)
}

/**
*
* Returns a binding ID by a [KProperty].
*
* @return A binding resource ID.
*/
internal fun KProperty<*>.bindingId(): Int {
return BindingManager.getBindingIdByProperty(this)
}

/**
*
* Returns a binding ID by a [KFunction].
*
* @return A binding resource ID.
*/
internal fun KFunction<*>.bindingId(): Int {
return BindingManager.getBindingIdByFunction(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.lnmcode.galleryapp.bindables

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingComponent
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment

abstract class BindingFragment<T : ViewDataBinding> constructor(
@LayoutRes private val contentLayoutId: Int
) : Fragment() {

/** This interface is generated during compilation to contain getters for all used instance `BindingAdapters`. */
protected var bindingComponent: DataBindingComponent? = DataBindingUtil.getDefaultComponent()

/** A backing field for providing an immutable [binding] property. */
private var _binding: T? = null

/**
* A data-binding property will be initialized in [onCreateView].
* And provide the inflated view which depends on [contentLayoutId].
*/
@BindingOnly
protected val binding: T
get() = checkNotNull(_binding) {
"Fragment $this binding cannot be accessed before onCreateView() or after onDestroyView()"
}

/**
* An executable inline binding function that receives a binding receiver in lambda.
*
* @param block A lambda block will be executed with the binding receiver.
* @return T A generic class that extends [ViewDataBinding] and generated by DataBinding on compile time.
*/
@BindingOnly
protected inline fun binding(block: T.() -> Unit): T {
return binding.apply(block)
}

/**
* Ensures the [binding] property should be executed and provide the inflated view which depends on [contentLayoutId].
*/
@CallSuper
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DataBindingUtil.inflate(inflater, contentLayoutId, container, false, bindingComponent)
return binding.root
}

/**
* Destroys the [_binding] backing property for preventing leaking the [ViewDataBinding] that references the Context.
*/
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.lnmcode.galleryapp.bindables

import androidx.databinding.Bindable
import java.util.*
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
import kotlin.reflect.full.hasAnnotation
import androidx.databinding.library.baseAdapters.BR

object BindingManager {

/** A map for holding information of the generated fields in the BR class. */
@PublishedApi
internal var bindingFieldsMap: Map<String, Int> = emptyMap()

/** Java Bean conventions for presenting a boolean. */
private const val JAVA_BEANS_BOOLEAN: String = "is"

/** Java Bean conventions for presenting a getter. */
private const val JAVA_BEANS_GETTER: String = "get"

/** Java Bean conventions for presenting a setter. */
private const val JAVA_BEANS_SETTER: String = "set"

/**
* Binds the `BR` class into the [BindingManager].
* This method only needs to be called once in the application.
* The `BR` class will be disassembled by the [BindingManager], binding fields will be used
* for finding the proper binding ID of properties.
*
* @param T The `BR` class that generated by the DataBinding processor.
* @return The size of the stored fields.
*/
inline fun <reified T> bind(): Int {
synchronized(this) {
if (bindingFieldsMap.isNotEmpty()) return@synchronized
bindingFieldsMap = BR::class.java.fields.asSequence()
.map { it.name to it.getInt(null) }.toMap()
}

return bindingFieldsMap.size
}

/**
* Returns proper binding ID by property.
*
* @param property A kotlin [androidx.databinding.Bindable] property for finding proper binding ID.
*/
internal fun getBindingIdByProperty(property: KProperty<*>): Int {
val bindingProperty = property.takeIf {
it.getter.hasAnnotation<Bindable>()
}
?: throw IllegalArgumentException("KProperty: ${property.name} must be annotated with the `@Bindable` annotation on the getter.")
val propertyName = bindingProperty.name.decapitalize(Locale.ENGLISH)
val bindingPropertyName = propertyName
.takeIf { it.startsWith(JAVA_BEANS_BOOLEAN) }
?.replaceFirst(JAVA_BEANS_BOOLEAN, String())
?.decapitalize(Locale.ENGLISH) ?: propertyName
return bindingFieldsMap[bindingPropertyName] ?: BR._all
}

/**
* Returns proper binding ID by function.
*
* @param function A kotlin [androidx.databinding.Bindable] function for finding proper binding ID.
*/
internal fun getBindingIdByFunction(function: KFunction<*>): Int {
val bindingFunction = function.takeIf {
it.hasAnnotation<Bindable>()
}
?: throw IllegalArgumentException("KFunction: ${function.name} must be annotated with the `@Bindable` annotation.")
val functionName = bindingFunction.name.decapitalize(Locale.ENGLISH)
val bindingFunctionName = when {
functionName.startsWith(JAVA_BEANS_GETTER) -> functionName.replaceFirst(JAVA_BEANS_GETTER, String())
functionName.startsWith(JAVA_BEANS_SETTER) -> functionName.replaceFirst(JAVA_BEANS_SETTER, String())
functionName.startsWith(JAVA_BEANS_BOOLEAN) -> functionName.replaceFirst(JAVA_BEANS_BOOLEAN, String())
else -> throw IllegalArgumentException("@Bindable associated with method must follow JavaBeans convention $functionName")
}.decapitalize(Locale.ENGLISH)
return bindingFieldsMap[bindingFunctionName] ?: BR._all
}
}
Loading

0 comments on commit 600c98d

Please sign in to comment.