728x90

RESTful API

  • RESTful API는 두 컴퓨터 시스템이 인터넷을 통해 정보를 안전하게 교환하기 위해 사용하는 인터페이스이다. 
  • 안전하고 신뢰할 수 있으며 효율적인 소프트웨어 통신 표준을 따르므로 이러한 정보 교환을 지원한다. 

REST

  • Representational State Transfer(REST)는 API 작동 방식에 대한 조건을 부과하는 소프트웨어 아키텍처이다. 
  • REST 아키텍처 스타일을 따르는 API를 REST API라고 한다. 
  • REST 아키텍처를 구현하는 웹 서비스를 RESTful 웹 서비스라고 한다.
    • RESTful API라는 용어는 일반적으로 RESTful 웹 API를 나타낸다. 하지만, REST API와 RESTful API라는 용어는 같은 의미로 사용할 수 있다.
  • REST 기반 아키텍처를 사용하여 대규모의 고성능 통신을 안정적으로 지원할 수 있다. 
  • 쉽게 구현하고 수정할 수 있어 모든 API 시스템을 파악하고 여러 플랫폼에서 사용한다.

API

  • 애플리케이션 프로그래밍 인터페이스(API)는 다른 소프트웨어 시스템과 통신하기 위해 따라야 하는 규칙을 정의
  • 개발자는 다른 애플리케이션이 프로그래밍 방식으로 애플리케이션과 통신할 수 있도록 API를 표시하거나 생성
    • ex) 근무 시간 기록 애플리케이션은 직원의 전체 이름과 날짜 범위를 요청하는 API를 표시 → 이 정보가 수신되면 내부적으로 직원의 근무 시간 기록을 처리하고 해당 날짜 범위에서 근무한 시간을 반환
  • API는 클라이언트와 리소스 사이의 게이트웨이라고 생각할 수 있다.

클라이언트

  • 클라이언트는 서버로부터 서비스를 요청하는 컴퓨터 또는 애플리케이션  
    • 웹 브라우저, 모바일 앱, 데스크톱 프로그램 등
  • 클라이언트는 API를 사용하는 사람이거나 소프트웨어 시스템일 수 있다. 
    • 웹 브라우저는 웹 서버로부터 웹 페이지를 요청하는 클라이언트
    • 모바일 앱은 API 서버로부터 데이터를 요청하는 클라이언트
    • 데스크톱 프로그램은 파일 서버로부터 파일을 요청하는 클라이언트

리소스

  • 서버가 제공하는 서비스 또는 데이터
    • 다양한 애플리케이션이 클라이언트에게 제공하는 정보이다.
  • 리소스는 이미지, 동영상, 텍스트, 숫자 또는 모든 유형의 데이터일 수 있다. 
  • 클라이언트에 리소스를 제공하는 시스템을 서버라고도 함
  • 조직은 API를 사용하여 리소스를 공유하고 보안, 제어 및 인증을 유지하면서 웹 서비스를 제공한다.

RESTful API는 어떻게 작동하는지

  • RESTful API의 기본 기능은 인터넷 브라우징과 동일
  • 클라이언트는 리소스가 필요할 때 API를 사용하여 서버에 접속
  1. 클라이언트가 서버에 요청을 전송
    • 클라이언트가 API 문서에 따라 서버가 이해하는 방식으로 요청 형식을 지정
  2. 서버가 클라이언트를 인증하고 해당 요청을 수행할 수 있는 권한이 클라이언트에 있는지 확인
  3. 서버가 요청을 수신하고 내부적으로 처리
  4. 서버가 클라이언트에 응답을 반환
    • 응답에는 요청이 성공했는지 여부를 클라이언트에 알려주는 정보가 포함
    •  응답에는 클라이언트가 요청한 모든 정보도 포함

RESTful API 클라이언트 요청에는 무엇이 포함되어 있는 것

고유 리소스 식별자

  • 서버는 고유한 리소스 식별자로 각 리소스를 식별
  • REST 서비스의 경우 서버는 일반적으로 URL(Uniform Resource Locator)을 사용하여 리소스 식별을 수행
  •  URL은 리소스에 대한 경로를 지정. URL은 웹페이지를 방문하기 위해 브라우저에 입력하는 웹 사이트 주소와 유사하다.
  • URL은 요청 엔드포인트라고도 하며 클라이언트가 요구하는 사항을 서버에 명확하게 지정한다.

메서드

HTTP 메서드는 클라이언트가 서버에 요청할 때 사용하는 명령어이다.

 

4가지 일반적인 HTTP 메서드 :


GET : 리소스를 조회 / 서버는 리소스의 표현을 응답으로 반환 (예: 웹 페이지를 요청)

  • 클라이언트는 GET을 사용하여 서버의 지정된 URL에 있는 리소스에 액세스 한다.
  • GET 요청을 캐싱하고 RESTful API 요청에 파라미터를 넣어 전송하여 전송 전에 데이터를 필터링하도록 서버에 지시할 수 있다.

POST : 새로운 리소스를 생성 / 클라이언트는 요청 본문에 새 리소스의 데이터를 포함한다. (예: 회원 가입)

  • 클라이언트는 POST를 사용하여 서버에 데이터를 전송한다.
    • 요청과 함께 데이터 표현이 포함
  • 동일한 POST 요청을 여러 번 전송하면 동일한 리소스를 여러 번 생성하는 부작용이 있다.


PUT : 기존 리소스를 수정 / 클라이언트는 요청 본문에 수정된 리소스의 데이터를 포함 (예: 프로필 정보 변경)

  • 클라이언트는 PUT을 사용하여 서버의 기존 리소스를 업데이트
  • POST와 달리, RESTful 웹 서비스에서 동일한 PUT 요청을 여러 번 전송해도 결과는 동일


DELETE : 리소스를 삭제 / 서버는 요청된 리소스를 삭제한다. (예: 게시물 삭제)

  • 클라이언트는 DELETE 요청을 사용하여 리소스를 제거한다.
  • DELETE 요청은 서버 상태를 변경할 수 있다.
    • 사용자에게 적절한 인증이 없으면 요청은 실패한다.

HTTP 헤더

  • 요청 헤더는 클라이언트와 서버 간에 교환되는 메타데이터이다.
  • 서버 간의 통신을 제어하고 정보를 전달하는 데 사용
  • 요청 헤더는 요청 및 응답의 형식을 나타내고 요청 상태 등에 대한 정보를 제공한다.
    • 요청하는 리소스의 식별자 (예: URL)
    • 요청하는 리소스의 형식 (예: HTML, JSON)
    • 클라이언트가 지원하는 언어
    • 클라이언트 인증 정보
    • 쿠키

데이터

REST API 요청에는 POST, PUT 및 기타 HTTP 메서드가 성공적으로 작동하기 위한 데이터가 포함될 수 있다.

파라미터

RESTful API 요청에는 수행해야 할 작업에 대한 자세한 정보를 서버에 제공하는 파라미터가 포함될 수 있다.

 

파라미터 유형

URL 세부정보를 지정하는 경로 파라미터.
리소스에 대한 추가 정보를 요청하는 쿼리 파라미터.
클라이언트를 빠르게 인증하는 쿠키 파라미터.


RESTful API 인증

RESTful 서비스는 응답을 보내기 전에 먼저 요청을 인증해야 한다. 인증은 신원을 확인하는 프로세스다.

RESTful API에는 4가지의 일반적인 인증 방법이 있다.

 

API 키

  • API 키는 REST API 인증을 위한 또 다른 옵션
  • 서버는 고유하게 생성된 값을 최초 클라이언트에 할당
  • 클라이언트는 리소스에 액세스 하려고 할 때마다 고유한 API 키를 사용하여 본인을 검증
  • API 키의 경우 클라이언트가 이 키를 전송해야 해서 네트워크 도난에 취약하기 때문에 덜 안전

OAuth

  • OAuth는 모든 시스템에 대해 매우 안전한 로그인 액세스를 보장하기 위해 암호와 토큰을 결합한다.
  • 서버는 먼저 암호를 요청한 다음 권한 부여 프로세스를 완료하기 위해 추가 토큰을 요청
  • 특정 범위와 수명으로 언제든지 토큰을 확인할 수 있다.

HTTP 인증

HTTP는 REST API를 구현할 때 직접 사용할 수 있는 일부 인증 체계를 정의한다. 

  • 기본 인증

기본 인증에서 클라이언트는 요청 헤더에 사용자 이름과 암호를 넣어 전송한다. 안전한 전송을 위해 이 페어를 64자의 세트로 변환하는 인코딩 기술인 base64로 인코딩한다.

  • 전달자 인증

전달자(bearer) 인증이라는 용어는 토큰 전달자에 대한 액세스 제어를 제공하는 프로세스를 나타낸다. 일반적으로 전달자 토큰은 서버가 로그인 요청에 대한 응답으로 생성하는 암호화된 문자열이다. 클라이언트는 리소스에 액세스 하기 위해 요청 헤더에 토큰을 넣어 전송한다.

 

 

 

참고

https://aws.amazon.com/ko/what-is/restful-api/

 

RESTful API란 무엇인가요? - RESTful API 설명 - AWS

Amazon API Gateway는 어떤 규모에서든 개발자가 API를 손쉽게 생성, 게시, 유지 관리, 모니터링 및 보안 유지할 수 있도록 하는 완전관리형 서비스입니다. API Gateway를 사용하면 실시간 양방향 통신 애

aws.amazon.com

 

728x90

주요 차이점 :

  • 모듈성 : Fragment는 보다 모듈화 되고 재사용 가능하며 , 여러 Activity에서 나타나는 UI 구성 요소에 이상적이다.
  • 생명 주기 : Activity는 여러 Fragment를 포함하고 관리할 수 있으며 독립적인 존재지만 , Fragment는 일반적으로 자신이 속한 Activity에 종속되어 있다.
  • UI 디자인의 유연성 : Activity는 전체 화면을 차지하고, Fragment는 전체 화면이 아닌 일부 화면으로
    구성할 수 있기 때문에 Fragment가 상대적으로 유연한 UI디자인이 가능하다.
    • 태블릿과 같은 대형 화면 레이아웃에서 여러 프래그먼트를 한 화면에 동시에 표시할 수 있다.

 

Activity는 앱의 다양한 섹션 또는 "화면" 을 제시하는 주요 방법이며 , Fragment는 정교한 사용자 인터페이스를 만드는 데 있어 동적이고 재사용 가능한 방법을 제공한다.

 

참고

https://velog.io/@asdsad8664/Activity-%EC%99%80-Fragment%EC%9D%98-%EC%B0%A8%EC%9D%B4

 

Activity 와 Fragment의 차이

Android Activity 와 Fragment의 차이점정의 : 안드로이드에서 액티비티는 사용자 인터페이스를 가진 단일 화면으로 작동합니다. 앱의 내용이 표시되는 프레임 또는 창과 같습니다. 각 액티비티는 독립

velog.io

 

728x90

Context 란 

Application/Activity 현재 정보, 흐름을 담고 있는 객체이며, 새로 생성된 객체에게 해당 실행 맥락을 제공한다.

 

  • Application 환경에 관한 전체 정보를 받을 수 있는 추상 클래스Activity, Brodcast Receiver, Service를 시작할 때도 사용되고 리소스에 접근할 때도 Context가 사용됨
  • Application과 관련된 정보에 접근 또는 Application과 연관된 시스템 레벨의 함수를 호출하고자 할 때 Context 필요 
  • 안드로이드는 Application 관련 정보를 ActivityManagerService에서 관리
    • Application과 관련된 정보에 접근하려고 할 때 식별자가 필요한데, 그 역할을 하는 것도 Context

 

Context의 구분(종류)

 

Application Context

  • Application의 맥락을 담고 있는 Context
  • 싱글톤 객체임
  • Activity에서 Context로 제공을 요청
  • 해당 Application과 생명 주기가 같고, Application 전역에서 유효
    • Application이 시작될 때 생성되고, 종료될 때 소멸 

 

applicationContext

 

Application Context = getApplicationContext() = getApplication()

  • Application의 생명 주기 내내 진행되는, 전역적인 작업 등의 Context로 적합
    • ActivityViewModel 클래스를 상속받는 viewModel을 사용하는 경우, ViewModel의 수명 주기가 Activity의 생명 주기보다 길기 때문에 사용할 수 있다
    • Activity에서 라이브러리를 초기화해야 할 경우 사용할 수 있다
    • Activity보다 생명 주기가 길기 때문에 Application Context를 사용한 작업이 진행된 후 메모리에서 Garbage Collection이 진행되지 않는다
  • Dialog 등의 UI를 표시하는 작업은 비효율적이거나, 아예 불가능하다
    • Dialog는 Activity의 수명 주기 내에서 충분히 처리할 수 있는 작업
    • Toast는 표시 가능

 

Activity Context

  • 실행중인 Activity의 맥락을 담고 있는 Context
  • 해당 Activity와 생명 주기가 같고, Activity 내에서만 유효
    • Activity가 시작될 때 생성되고, 종료될 때 소멸 

this = baseContext 

Activity Context = this = getBaseContext()

  • this 또는 baseContext는 현재 Activity 자신을 가리킨다. 
  • Activity의 생명 주기 내에서 수행하는 작업에 제공되는 Context로 적합하다. 
    • Intent, Toast 등에서 사용하기 적합
  • Fragment에서의

 

getContext()와 requireContext()

  • 가장 큰 차이는 nullable 여부
  • requireContext : NonNull
  • getContext : Nullable
  • 반환 : Activity Context

단지 반환값의 차이로, context를 불러올 수 없는 경우 null을 반환하는지, 아니면 throw를 통해 exception을 발생시키는지에 대한 차이이다. (동일한 코드지만 exception을 일으키느냐, null을 반환하느냐)

 

Null이 아님을 보장해준다고 생각하기 쉬운 requireContext()를 쓰더라도 터지지 않는 것은 아님

예외처리 구문을 이용하든, if(context == null)을 이용하든 context는 특정 상황에서 null이 될 수 있음을 기억하고 (스레드나 코루틴을 통한 비동기적인 코드를 작성하는 측면) 적절한 처리가 필요하다. 

 

 

※ Context가 들어가는 자리에 적절하지 않은 Context를 사용한다면 메모리 누수가 발생할 수 있다.

ex) Application Context를 참조하여 싱글톤 객체를 생성해야 할 때 Activity Context를 참조하여 생성하게 된다면, Garbage Collector가 Activity Context를 수거하지 못하여 메모리 누수가 발생 

 

Activity의 생명 주기 내에서 처리할 수 있는 작업은 (Intent, Toast, Dialog 등) this, baseContext 와 같은 Activity Context를 사용하여 작업을 처리하고, ViewModel 등의 Application과 생명 주기를 공유하거나 단일 Activity의 생명 주기보다 긴 작업을 하는 경우에는 Application Context를 사용해야 함 

 

 

 

 

참고

https://velog.io/@kajpjs0508/Android-Context%EC%99%80-Context%EC%9D%98-%EA%B5%AC%EB%B6%84-baseContext-applicationContext-this

 

[Android] Context, 적시 사용법(baseContext, applicationContext, this)

안드로이드 Context의 적시 사용법

velog.io

 

728x90

ANR이란 Application Not Responding의 약자로, '애플리케이션이 응답하지 않는다'는 뜻이다. 

 

ANR 발생 요인 

  • 애플리케이션이 UI 스레드에 어떠한 I/O 명령(빈번한 네트워크 액세스)으로 인해 막힐 때
  • 너무 많은 시간을 정교한 메모리 구조를 구축하는데 들일 때
  • Input 이벤트(키를 누르거나 화면을 터치하는 등)에 5초안에 반응을 하지 않을 때
  • BroadcatReceiver 가 10초내로 실행을 끝내지 않을 때 (UI가 없는 브로드캐스트 리시버, 서비스도 실행 주체가 메인스레드이므로 긴 시간을 소모하는 작업인 경우 ANR을 발생시킨다.)

 

ANR 발생 방지 

  • ANR의 발생을 예방하려면 소요 시간이 긴 작업을 최소화해야 한다. 
  • UI업데이트를 제외한 나머지 작업은 메인 스레드에서 작업하지 않는다.
    • 연산, 네트워크 통신, DB와 관련된 작업 등은 굳이 사용자에게 시각적으로 보여주지 않고 해당 작업의 결과만 알려주면 되기 때문 
    • 작업이 수행되고 있음을 사용자가 알 수 있도록 프로그래스바 등으로 표현해 주는 것이 좋음  

특정 기능을 수행하는 버튼을 눌렀을 때 기능이 수행되고 있음에도 시각적으로 알 수가 없다면, 사용자가 기능을 반복해서 실행시킬 수 있어서 결과적으로 작업 과부하로 인해 ANR이 발생하거나, 사용자가 문제가 있다고 생각할 수 있고 앱 자체를 꺼버릴 수 있음 

 

결론

메인 UI 스레드가 오래 기다리지 않게 한다.
복잡한 작업이 있다면 백그라운드에서 진행한다.

 

 

참고

https://itmining.tistory.com/3

 

[안드로이드] ANR의 의미와 예방

이 글은 PC 버전 TISTORY에 최적화 되어있습니다. 서론 안드로이드 앱을 구현하여 돌리다보면, 또는 플레이스토어에 올라온 앱임에도 불구하고, 앱의 중지를 알리는 메시지를 심심치 않게 봤을 것

itmining.tistory.com

 

728x90

Parcelable과 Serializable은 객체를 직렬화하여 전달하기 위한 방법이다.

 

직렬화 : 객체들의 데이터를 연속적인 데이터(스트림)로 변형하여 전송 가능한 형태로 만드는 것
역직렬화 : 직렬화된 데이터를 다시 객체의 형태로 만드는 것

 

객체 데이터를 통신하기 쉬운 포맷(Byte, CSV, Json..) 형태로 만들어주는 작업을 직렬화라고 볼 수 있고,
역으로, 포멧(Byte,CSV,Json..) 형태에서 객체로 변환하는 과정을 역직렬화라고 할 수 있겠다.

 

(이해에 도움이 될 수도 있음)

Parcel 이란 한국어로 꾸러미 라는 뜻이다. 객체를 싸는 클래스가 바로 Parcel 클래스 

Parcel 이 필요한 이유

안드로이드에서 프로세스 간 통신을 위해 Bundle 을 사용하는데, Key 와 Value 쌍으로 이루어져 있는 형태이다. 

이러한 Bundle 에서 원시타입(Primitive Type) Int, String, boolean.... 같은 간단한 데이터는 그대로 Value 값으로 넣을 수 있지만, Kotlin의 data class 같은 것들은 내부에 많은 데이터가 들어 있기 때문에 Map에 Value 값으로 입력하는 것이 어렵다. 

이런 문제가 있어서 Bundle Value 값으로 입력될 수 있는 것이 Parcel 

 

 

Parcelable와 차이 

Parcelable

  • Parcelable은 안드로이드에서 제공하는 인터페이스로, 객체를 전달하기 위해 사용
  • Parcelable 인터페이스를 구현한 객체는 안드로이드 OS에서 Intent나 Bundle에 담아 전달할 수 있음
  • Parcelable을 사용하면 객체를 직렬화하여 안드로이드 OS에서 처리할 수 있는 바이트 배열로 변환하여 전달

Serializable

  • Serializable은 Java에서 제공하는 인터페이스로, 객체를 직렬화하여 전달하기 위해 사용
  • Serializable을 사용하면 객체를 바이트 스트림으로 변환하여 전달할 수 있음
  • Serializable 인터페이스를 구현한 객체는 Java에서 제공하는 ObjectOutputStream을 사용하여 전달할 수 있음

1. 속도 

  • Parcelable이 Serializable보다 빠름
  • Paecelable은 안드로이드 OS에서 직접 처리하기 때문에 직렬화와 역직렬화 시간이 적고, Serializable은 Java의 Reflection을 사용하기 때문에 상대적으로 느림
  • Serializable은 ObjectOutputStream과 ObjectInputStream을 사용하기 때문에 객체를 직렬화하는 시간이 더 길어 짐

2. 크기 

  • Parcelable은 Java의 Serializable보다 객체를 직렬화할 때 생성되는 데이터 크기가 작음
    • 객체의 멤버 변수들을 Parcel에 쓰기 때문에 필요한 데이터만 쓰게 됨 
  • Serializable은 객체의 모든 데이터를 직렬화하기 때문에 불필요한 데이터까지 모두 쓰게 되어서 상대적으로 데이터 크기가 큼

3. 안정성

  • Parcelable은 Serailizable 보다 안정성이 높음
    • Parcelable은 직렬화와 역직렬화에 대한 오류 검사를 수행하기 때문

4. 그 외

  • Parcelable은 안드로이드 OS에서만 사용할 수 있음
    • Parcelable을 사용하여 전달한 객체는 안드로이드 애플리케이션에서만 사용할 수 있음
    • 객체 크기가 작고 안드로이드 OS에서만 사용하는 경우 Parcelable을, 객체 크기가 크거나 다른 플랫폼과도 호환해야 하는 경우 Serializable을 사용하는 것이 좋음 

 

 

 

728x90

APK 

  • Android Applickation Package의 약자 
  • apk라고 하면 안드로이드 앱의 확장자이다. 
  • 안드로이드 OS에서 앱을 설치하는 용도로 apk 파일을 주로 사용 
  • APK 확장 파일을 다운로드하여 기기에 저장하면 구글 플레이 스토어에서 다운로드하지 않아도 앱을 실행할 수 있음  
  • apk 파일을 직접 다운로드 하는 경우에는 보안이 취약하다. 

구글 플레이에서 사용자가 다운로드 하는 압축된 APK는 100MB 이하여야 하는데, 고화질의 그래픽, 미디어 파일 같은 대용량 저작물을 저장할 공간이 많이 필요할 경우, 100MB를 초과하게 됨 결국 파일의 크기가 커지는데 이런 문제를 보완한 것이 AAB이다. 

 

결국 apk의 용량문제를 해결하기 위해 개발됨 

 

AAB

  • Android App Bundle의 약자 
  • 앱의 압축 크기가 100MB를 넘어도 확장파일이 필요없다. 
  • 최대 150MB까지 가능하고, APK 생성 및 구글 플레이 서명이 연기된다는 장점을 갖춤

APK와 비교했을 때 가장 좋은 점은 앱 실행에 필요없는 모든 리소스를 다운로드하는 APK와는 달리 앱 실행에 필요한 코드와 리소스만 담긴 최적화 APK를 다운로드할 수 있다는 점이다. 

 

AAB 단점 

보안문제 

 

모든 안드로이드 앱에는 개발자의 서명 파일이 들어간다. 서명 파일은 개발자가 APK 파일 내에 직접 첨부 하기 때문에 앱의 불법 배포와 같은 부분을 방지할 수 있었지만 AAB 파일은 구글 플레이에서 최적화 APK를 생성한 후 서명을 구글플레이가 대신하기 때문에 대리 서명이 될 수밖에 없다.

만약 최적화에 이상이 생긴다면 그 결과에 대한 불만은 개발자의 몫으로 돌아오게 된다는 점에서 우려가 있다.

 

시장 독점 

 

AAB 확장자를 사용하게 되면 필수로 구글 플레이 스토어만을 통해 앱 배포가 가능해진다.

  • 타 스토어를 통해 배포하려면 apk 확장자, aab 확장자 둘 다 제작해야 하는 경우가 생길 수 있음 

 

 

728x90

뷰페이저 무한 스크롤

 

기존의 자동 페이징 기능만 있던 뷰 페이저를 계속 스크롤할 수 있게 변경했다. 

구글링을 해서 찾아본 결과 지원하는 메서드는 따로 없는 것 같고, 눈 속임을 하는 듯하다. 

 

설명 예시1
설명 예시2

 

 

무한 스크롤 검색 예제

검색으로 자료를 찾아 봤을 때 예제들이 거의 이런 방식이다. 

override fun getItemCount(): Int = Int.MAX_VALUE

Int가 표현할 수 있는 Max인 억 자리 숫자를 return 해주고, position을 list의 크기로 나눈 나머지 값을 사용하면 계속 스크롤되는 것처럼 보이는 편법을 사용하는 듯하다. 

 

코드적용 

 

UpcomingShowAdapter 

더보기
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.nbc.curtaincall.databinding.ItemUpcomingShowBinding
import com.nbc.curtaincall.fetch.model.DbResponse


class UpcomingShowAdapter(private val listener: PosterClickListener? = null) :
    ListAdapter<DbResponse, UpcomingShowAdapter.UpcomingShowViewHolder>(object :
        DiffUtil.ItemCallback<DbResponse>() {
        override fun areItemsTheSame(oldItem: DbResponse, newItem: DbResponse): Boolean {
            return oldItem.mt20id == newItem.mt20id
        }

        override fun areContentsTheSame(oldItem: DbResponse, newItem: DbResponse): Boolean {
            return oldItem == newItem
        }
    }) {
    inner class UpcomingShowViewHolder(private val binding: ItemUpcomingShowBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: DbResponse) {
            with(binding) {
                Glide.with(itemView).load(item.poster).into(ivHomeUpcomingShowPoster)
                tvPerformanceName.text = item.prfnm
                tvPeriod.text = "${item.prfpdfrom} ~ ${item.prfpdto}"
                tvFacilityName.text = item.fcltynm
                ivHomeUpcomingShowPoster.setOnClickListener {
                    item.mt20id?.let { id -> listener?.posterClicked(id) }
                }
            }
        }
    }
    //Int.MAX_VALUE 가 아닌 받아오는 list의 사이즈 의 제곱을 리턴 해줌
    override fun getItemCount(): Int = currentList.size * currentList.size
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UpcomingShowViewHolder {
        val inflater =
            parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val binding = ItemUpcomingShowBinding.inflate(inflater, parent, false)
        return UpcomingShowViewHolder(binding)
    }

    override fun onBindViewHolder(holder: UpcomingShowViewHolder, position: Int) {
        holder.bind(currentList[position % currentList.size])
    }
}

 

HomeFragment

더보기
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.nbc.curtaincall.R
import com.nbc.curtaincall.databinding.FragmentHomeBinding
import com.nbc.curtaincall.fetch.network.retrofit.RetrofitClient.fetch
import com.nbc.curtaincall.fetch.repository.impl.FetchRepositoryImpl
import com.nbc.curtaincall.presentation.home.HorizontalMarginItemDecoration
import com.nbc.curtaincall.ui.home.adapter.GenreAdapter
import com.nbc.curtaincall.ui.home.adapter.KidShowAdapter
import com.nbc.curtaincall.ui.home.adapter.PosterClickListener
import com.nbc.curtaincall.ui.home.adapter.TopRankAdapter
import com.nbc.curtaincall.ui.home.adapter.UpcomingShowAdapter
import com.nbc.curtaincall.ui.main.MainViewModel
import com.nbc.curtaincall.ui.main.MainViewModelFactory
import com.nbc.curtaincall.ui.ticket.TicketDialogFragment
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class HomeFragment : Fragment(), PosterClickListener {
    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!
    private val viewModel: HomeViewModel by viewModels {
        HomeViewModelFactory(
            fetchRemoteRepository = FetchRepositoryImpl(fetch),
        )
    }
    private val sharedViewModel: MainViewModel by activityViewModels<MainViewModel> {
        MainViewModelFactory(
            fetchRemoteRepository = FetchRepositoryImpl(
                fetch
            )
        )
    }
    private val upComingShowAdapter: UpcomingShowAdapter by lazy { UpcomingShowAdapter(this) }
    private val topRankAdapter: TopRankAdapter by lazy { TopRankAdapter(this) }
    private val genreAdapter: GenreAdapter by lazy { GenreAdapter(this) }
    private val kidShowAdapter: KidShowAdapter by lazy { KidShowAdapter(this) }
    private var isPaging = false
    private var pagingJob: Job? = null
    private val onPageChangeCallback: OnPageChangeCallback = object : OnPageChangeCallback() {
        //페이지가 선택될 때 마다 호출
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            //인디케이터를 표시하기 위한 코드 EX) (1/10)
            binding.tvPageIndicator.text =
                "${(position % upComingShowAdapter.currentList.size) + 1} / ${upComingShowAdapter.currentList.size}"
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        initViews()
        setUpObserve()
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        with(viewModel) {
            //공연 예정작
            fetchUpcoming()
            //TOP 10 공연
            fetchTopRank()
            //장르 스피너 선택
            binding.spinnerHomeGenre.setOnSpinnerItemSelectedListener<String> { _, _, newIndex, _ ->
                fetchGenre(newIndex)
            }
            //어린이 관람 가능 공연 목록
            fetchKidShow()
        }
    }

    //화면 초기 설정
    private fun initViews() {
        //어뎁터 초기화
        upComingShowAdapter
        topRankAdapter
        kidShowAdapter
        initRecyclerView()

        //viewpager 연결
        with(binding.viewPager) {
            adapter = upComingShowAdapter
            //ViewPager PageTransformer 세팅
            offscreenPageLimit = 1
            setPageTransformer(SliderTransformer(requireContext()))
            val itemDecoration = HorizontalMarginItemDecoration(
                requireContext(),
                R.dimen.viewpager_current_item_horizontal_margin
            )
            addItemDecoration(itemDecoration)

            //PageChangeCallback
            registerOnPageChangeCallback(onPageChangeCallback)

        }
        //장르 연극 초기화
        viewModel.fetchGenre(0)
    }

    //옵저브 세팅
    private fun setUpObserve() {
        with(viewModel) {
            showList.observe(viewLifecycleOwner) {
                upComingShowAdapter.submitList(it)
                //포지션을 중간위치에 맞추기 위한 코드 / 추가된 리스트의 크기가 짝수/홀수 일때 처리 (인디케이터) 
                binding.viewPager.currentItem =
                    if (upComingShowAdapter.itemCount % 2 == 0) (upComingShowAdapter.itemCount / 2)
                    else (upComingShowAdapter.itemCount / 2) - (upComingShowAdapter.currentList.size / 2)
                //페이징 초기화
                if (!isPaging) startPaging()
            }
            topRank.observe(viewLifecycleOwner) {
                topRankAdapter.submitList(it?.take(10))
            }
            genre.observe(viewLifecycleOwner) {
                genreAdapter.submitList(it)
            }
            kidShow.observe(viewLifecycleOwner) {
                kidShowAdapter.submitList(it)
            }
            //로딩 화면 처리
            isLoadingGenre.observe(viewLifecycleOwner) {
                binding.skeletonGenreLoading.isVisible = !it
            }
            isLoadingRecommend.observe(viewLifecycleOwner) {
                binding.skeletonTopRankLoading.isVisible = !it
            }
            isLoadingKid.observe(viewLifecycleOwner) {
                binding.skeletonKidLoading.isVisible = !it
            }

        }
    }

    //리사이클러뷰 초기화
    private fun initRecyclerView() {
        with(binding) {
            //HOT 추천 리사이클러뷰
            rvHomeTopRank.apply {
                adapter = topRankAdapter
                layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
            //장르별 리사이클러뷰
            rvHomeGenre.apply {
                adapter = genreAdapter
                layoutManager =
                    LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
            //어린이 공연 리사이클러뷰
            rvHomeKidShow.apply {
                adapter = kidShowAdapter
                layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
        }
    }

    //3초 후 자동 페이징
    private fun nextPage() {
        runCatching {
            with(binding) {
            	//스크롤이 끝까지 닿았을 경우 
                if (viewPager.currentItem == upComingShowAdapter.itemCount - 1) {
                    lifecycleScope.launch {
                        delay(3000)
                    }
                    viewPager.currentItem =
                        (upComingShowAdapter.itemCount / 2) - (upComingShowAdapter.currentList.size / 2)
                } else {
                    viewPager.currentItem++
                }
            }
        }
    }

    //페이징 스타트 함수
    private fun startPaging() {
        isPaging = true
        pagingJob = lifecycleScope.launch {
            while (true) {
                delay(3000)
                nextPage()
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        isPaging = false
        pagingJob?.cancel()
    }

    //포스터 클릭 시 티켓
    override fun posterClicked(id: String) {
        val ticketDialog = TicketDialogFragment()
        sharedViewModel.sharedShowId(id) //해당 공연의 id를 MainViewModel로 보내줌
        ticketDialog.setStyle(
            DialogFragment.STYLE_NORMAL,
            R.style.RoundCornerBottomSheetDialogTheme
        )
        ticketDialog.show(childFragmentManager, ticketDialog.tag)
    }
}

 

메모리 누수가 생겨서 

 

 

참고 

https://medium.com/@ashishpandey.professional/android-infinite-scrolling-viewpager2-195e3786ec3f

 

Android Infinite scrolling ViewPager2

Achieve the smooth bi-directional infinite scrolling ViewPager 2

medium.com

 

728x90

Service 

안드로이드 4대 컴포넌트 중 하나이며, 사용자와 상호작용하지 않고 백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 구성 요소이다.

 

  • 사용자 인터페이스를 제공하지 않는다. 
  • Service가 시작되면 사용자가 다른 애플리케이션으로 전환한 후에도 한동안 서비스가 계속 실행될 수 있다.
  • 구성요소는 Service에 바인딩되어 서비스와 상호작용하고 프로세스 간 통신 (IPC)을 실행할 수도 있다.
    •  Service는 백그라운드에서 네트워크 트랜잭션을 처리하고, 음악을 재생하거나, 파일 I/O를 실행하거나, 콘텐츠 제공업체와 상호작용할 수 있습니다.
  • 포그라운드 서비스
    • 사용자가 입력한 명령이 실행되어 결과가 출력될 때까지 기다려야 하는 방식으로 처리되는 서비스
    • 유저가 현재 뭘 하고 있다는 걸 인지하고 있는 서비스
    • 메모리 부족 시 안드로이드 시스템에 의한 강제종료 대상에서 제외됨
    • 포그라운드 서비스가 실행되는 도중엔 무조건 상태바에 알림이 제공돼야 함
    • 앱과 사용자가 상호작용하지 않아도 계속 실행됨
    • 포그라운드 프로세스 : 화면에서 앱이 어떤 작업을 하는지 눈으로 볼 수 있는 프로세스(유튜브 시청, 웹 서핑)
  • 백그라운드 서비스
    • 사용자에게 안 보이는 작업을 수행
    • 인스타에서 게시물 공유 버튼을 눌러 인스타에 사진을 업로드하는 동안 카톡하거나 인스타를 꺼도 게시물 업로드는 종료되지 않는다. 이 때 백그라운드 서비스가 사용됨
  • 바인드 서비스
    • 클라이언트-서버 구조처럼 컴포넌트가 서비스에 연결되면 서비스가 서버 역할을 함
    • 컴포넌트가 서비스에게 요청하면 서비스는 그것에 맞는 결과값을 리턴 함 
    • 클라이언트가 연결을 해제하고 서비스와 연결된 클라이언트가 없을 때 바인드 서비스가 종료

Service의 생명 주기

  • onCreate() : 서비스가 처음 생성되었을 경우 onStartCommand() 혹은 onBind()가 호출되기전에 호출된다. 서비스가 이미 실행중일 경우 이 메소드는 호출 x
  • onStartCommand() : 시스템에서 액티비티와 같이 다른 컴포넌트에서 startService()를 호출하게 되면 이 메소드가 실행되고 서비스가 시작된다. 이 메소드를 구현한 후 서비스를 중단하기 위해 stopSelf()나 stopService() 메소드를 호출해야 한다. 바인딩만 할 경우 해당 메소드 구현할 필요 없음

 

onStartCommand()의 return 값

 

START_NOT_STICKY: 시스템이 서비스를 onStartCommand()를 반환 후에 중단시키면 서비스를 재생성 하면 안됨. 서비스가 불필요하게 여러개 생성되는 것을 막을수 있는 방법

START_STICKY: 시스템이 onStartCommand() 반환 후에 서비스를 중단하면 서비스를 자동으로 다시 생성하고 마지막 인텐트는 전달하지 않음.

START_REDELIVER_INTENT: 시스템이 onStartCommand()를 반환 후에 서비스를 중단하는 경우, 서비스를 다시 생성하고 이 서비스에 전달된 마지막 인텐트로 onStartCommand()를 호출하면 모든 보류 인텐트가 차례로 전달

(즉시 재개되어야 하는 작업을 수행할 때(예: 파일 다운로드)에 적합

  • onBind(): 안드로이드의 구성요소가 서비스에 바인딩하고자 하는 경우, 이 메소드가 호출

시스템에서 액티비티와 같이 다른 컴포넌트에서 bindService()를 호출하게 되면 이 메소드가 실행되고 서비스에 바인딩 된다.

이 메소드의 구현체에서는 IBinder를 리턴하여 클라이언트와 서비스가 통신할 수 있는 인터페이스를 제공해야 한다.

항상 이 메소드를 구현해야 하며, 바인딩을 원하지 않으면 null을 반환하는 식으로라도 구현해야 한다.

 

Manifest에 서비스를 activity와 유사하게 등록을 해주어야 한다.

 

 

참고자료 

https://developer.android.com/guide/components/services?hl=ko 

 

서비스 개요  |  Background work  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 서비스 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Service는 백그라운드에서 장기 실행 작업

developer.android.com

https://onlyfor-me-blog.tistory.com/380

 

[Android] 서비스란?

서비스는 안드로이드 4대 컴포넌트 중 하나로, 백그라운드에서 어떤 작업을 수행해야 할 때 사용을 고려할 수 있는 컴포넌트다. 그러나 백그라운드에서 수행되는 작업이라고 다 서비스를 사용

onlyfor-me-blog.tistory.com

 

728x90

적용화면

 

준비 

1. 스플래시 이미지 찾기

https://lottiefiles.com/

 

LottieFiles: Download Free lightweight animations for website & apps.

Effortlessly bring the smallest, free, ready-to-use motion graphics for the web, app, social, and designs. Create, edit, test, collaborate, and ship Lottie animations in no time!

lottiefiles.com

사용할 애니매이션

2. 사용할 애니메이션 다운로드 (Json 파일) 

 

3. lottie 파일 res/raw 안에 대문자 없이 넣어주기

 

4. AndroidManifest.xml 앱 실행 루트 변경 -> ex) SplashActivity 

 

5. 라이브러리 추가 

        // lottie
	implementation ("com.airbnb.android:lottie:6.3.0")

	// android splash
	implementation("androidx.core:core-splashscreen:1.0.1")

 

 

 

 

SplashActivity

더보기
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.nbc.curtaincall.R
import com.nbc.curtaincall.ui.main.MainActivity

class SplashActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_splash)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        lifecycleScope.launch {
            delay(3000)
            startActivity(Intent(this@SplashActivity, MainActivity::class.java))
            finish()
        }
    }
}

activity_splash.xml 

더보기
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    tools:context=".presentation.main.SplashActivity">

    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/lottie"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:lottie_autoPlay="true"
        app:lottie_loop="true"
        app:layout_constraintBottom_toTopOf="@id/tv_title"
        app:lottie_rawRes="@raw/lottie_splash" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/black"
        android:text="ShowNect"
        android:textColor="@color/primary_color"
        android:textSize="40sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

라이브러리추가

 

 

-----수정-----

 

themes.xml 

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.CurtainCall" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor">@color/background_color</item>
        <!-- Customize your theme here. -->
        <item name="android:windowBackground">@color/black</item>
        <item name="android:windowIsTranslucent">true</item> //추가 해줘야 스플래시 이중으로 안뜸
    </style>
    
</resources>

 

728x90

TicketDialogFragment / OnViewCreated

//Swipe Gesture
        binding.layoutSimpleScrollview.setOnTouchListener(object :
            OnSwipeTouchListener(requireContext()) {
            override fun onSwipeTop() {
                super.onSwipeTop()
                val intent = Intent(context, DetailActivity::class.java).apply {
                    putExtra(Constants.SHOW_ID, ticketId)
                    putExtra(Constants.FACILITY_ID, facilityId)
                }
                startActivity(intent)
                activity?.overridePendingTransition(R.anim.slide_up, R.anim.no_animation)
            }
        })
    }

 

onSwipeTop() 오버라이드해서 onSwipeTop() 호출될때의 코드 작성 

예시에는 DetailActivity를 띄워주는 코드 

 

activity?.overridePendingTransition(R.anim.slide_up, R.anim.no_animation) 

- 액티비티 띄울 때 애니매이션 적용 (아래에서 위로)

 

res/anim/slide_up.xml / res/anim/no_animtaion.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromYDelta="100%"
        android:toYDelta="0%" />
</set>

//no_animation.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

</set>

 

 

OnSwipeTouchListener 

더보기
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
//Gesture
open class OnSwipeTouchListener(ctx: Context) : View.OnTouchListener {

    private val gestureDetector: GestureDetector

    companion object {
        private val SWIPE_THRESHOLD = 500 // 스와이프 거리를 늘릴 값
        private val SWIPE_VELOCITY_THRESHOLD = 500 // 스와이프 인식에 필요한 최소 속도를 늘릴 값
    }

    init {
        gestureDetector = GestureDetector(ctx, GestureListener())
    }

    private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {

        override fun onDown(e: MotionEvent): Boolean {
            return true
        }

        override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            var result = false
            try {
                val diffY = e2.y - e1!!.y
                val diffX = e2.x - e1.x
                if (Math.abs(diffX) > Math.abs(diffY)) {
                    if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
                        if (diffX > 0) {
                            onSwipeRight()
                        } else {
                            onSwipeLeft()
                        }
                        result = true
                    }
                } else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
                    if (diffY > 0) {
                        onSwipeBottom()
                    } else {
                        onSwipeTop()
                    }
                    result = true
                }
            } catch (exception: Exception) {
                exception.printStackTrace()
            }

            return result
        }

    }

    fun onSwipeRight() {}

    fun onSwipeLeft() {}

    open fun onSwipeTop() {}

    fun onSwipeBottom() {}
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        try {
            return gestureDetector.onTouchEvent(event!!)
        } catch (e: Exception) {
            // Error Handling
        }
        return false
    }
}

 

SimpleOnGestureListener

  • 제스처 이벤트를 감지하는 데 사용되는 여러 콜백 메서드를 기본 구현으로 제공

onDown(e: MotionEvent): Boolean

  • 메서드는 모든 제스처 이벤트가 시작될 때 호출 (true를 반환하여 이벤트 시퀀스를 처리)

onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean

  • onFling(스크롤과 비슷하지만 손가락으로 튕길 때)
  • 플링 제스처가 발생했을 때 호출 

 

참고 

https://evolog.tistory.com/7

 

[코틀린] Kotlin Swipe 동작 구현하기 (OnSwipeTouchListener)

개요 Kotlin 코드로 View에 Swipe 동작 구현 코드 정리 입니다. 매번 검색하기 번거로워 정리하려 합니다. 핵심은 OnSwipeTouchListener Class 구현 코드를 정리 입니다. (※ 참고 : https://stackoverflow.com/questions/

evolog.tistory.com

 

+ Recent posts