Android/StoreInfo

<강의> 안드로이드 앱 개발 숙련 1주차

re트 2024. 1. 4. 11:43
728x90

1. 뷰 바인딩

혼자서 앱 개발을할 때는 그냥 검색해서 편하다고 하니까 그냥 썼었다.

그런데 강의에 나오기도 했으니 정리하고 지나가야겠다.

1) 소개

뷰 바인딩이 나오게 된 가장 주된 이유는 findViewById를 대체하기 위해서다.

정말로 코드를 작성하다보면 뷰 객체를 많이 만들게 되는데 이를 초기화할 때마다 findViewById를 쓰고 있자면 좀 귀찮았다.

그런데 뷰 바인딩을 쓰게 되면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있을 뿐만 아니라 짧고 간결하고 가독성 좋게 작성할 수 있다.

안 쓸 이유가 없다고 생각한다.

 

모듈에서 사용 설정된 뷰 바인딩은 모듈에 있는 각 xml 레이아웃 파일의 결합 클래스를 생성한다고 하는데 코틀린 코드 파일과 xml 레이아웃 파일을 연결시킨다고 생각하면 될 거 같다.

 

안드로이드 스튜디오는 레이아웃 파일의 이름을 기반으로 한 바인딩 클래스를 자동으로 생성한다.

바인딩 객체의 이름은 레이아웃 파일 이름에 'Binding'을 붙여 만들어진다.

바인딩 클래스의 인스턴스에는 상응하는 레이아웃에 ID가 있는 모든 뷰의 직접 참조가 포함되기 때문에 속성에 대한 접근 및 변경이 가능하다.

또한 직접 참조할 수 있게 해주는 안전한 코드를 자동으로 생성해준다.

이것을 통해 Null 안전성을 가지게 되고 null 값으로 인한 오류, 뷰가 아직 화면에 나타나지 않았는데 그 뷰를 사용하려고 할 때 생길 수 있는 문제들을 예방할 수 있다.

 

xml 레이아웃 파일에서 정의된 뷰의 타입과 자동 생성된 바인딩 클래스의 타입이 항상 일치하기 때문에, 초기화시 타입이 서로 맞지 않아 발생할 수 있는 오류를 방지한다.

 

2) 설정

gradle 설정

android{
	...
    
    // AndroidStudio 3.6 ~ 4.0
    viewBinding{
    	enabled = true
    }
    
    // AndroidStudio 4.0 ~
    buildFeatures{
    	viewBinding = true
    }
}

액티비티 설정

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // inflate는 xml에 있는 뷰를 객체화해준다고 생각하면 됨
        binding = ActivityMainBinding.inflate(layoutInflater)
        // 객체화하여 생성한 루트 뷰를 넘겨줌
        setContentView(binding.root)
        
        // binding.<뷰 아이디>.<속성> ...
        binding.myButton.setOnClickListener {
            binding.myTextView.text = "바인딩이 잘 되네요!"
        }
}

 

2. AdapterView

이제는 어댑터를 사용하는 뷰들이 나오기 시작한다.

긴장된다...

1) 어댑터뷰

여러개의 항목을 다양한 형식으로 나열하고 선택할 수 있는 기능을 제공하는 뷰

종류에는 항목을 수직으로 나열시키는 방식을 가지는 리스트뷰와 항목을 격자 형태로 나열시키는 방식을 가지는 그리드뷰가 있다.

어댑터뷰는 표시할 항목 데이터를 직접 관리하지 않고, 어댑터라는 객체로부터 공급 받는다.

어댑터뷰는 어댑터에 정의된 인터페이스를 바탕으로 필요한 정보를 요청하여 항목 뷰를 화면에 표시하거나 선택된 항목 뷰를 처리한다.

어댑터뷰가 데이터 항목을 표시하는 과정

  -> 데이터 원본을 어댑터에 설정하고, 어댑터를 어댑터뷰에 설정한다.

  -> 표시할 항목의 총 개수를 알기 위해 어댑터뷰는 어댑터의 getCount() 메소드를 통해 현재 어댑터가 관리하는 데이터 항목의 총 개수를 가져온다.

  -> 어댑터뷰가 어댑터의 getView() 메소드를 통해서 화면에 실제로 표시할 항목 뷰를 얻고, 이를 화면에 표시한다.

어댑터가 선택 이벤트를 처리하는 과정

  -> 사용자가 어댑터뷰의 특정 위치의 항목을 선택

  -> 선택된 항목, 항목 ID, 항목 뷰를 어댑터의 getItem(), getItemId(), getView() 메소드를 통해 얻어옴

  -> 얻어온 결과값을 항목 선택 이벤트 처리기에 넘겨줌

 

2) 어댑터

데이터를 관리하는 역할

데이터 원본과 어댑터뷰 사이의 중계 역할

 

3) 어댑터의 종류

BaseAdapter 

  - 어댑터 클래스의 공통 구현

  - 사용자 정의 어댑터 구현 시 사용

ArrayAdapter

  - BaseAdapter 확장

  - 객체 배열이나 리소스에 정의된 배열로부터 데이터를 공급받음

CursorAdapter

  - BaseAdapter 확장

  - 데이터베이스로부터 데이터를 공급받음

SimpleAdapter

  - BaseAdapter 확장

  - 데이터를 Map(키, 값)의 리스트로 관리

  - 데이터를 xml 파일에 정의된 뷰에 대응시키는 어댑터

 

3. ListView

팀과제 하면서 스크롤뷰 대신 사용하려다가 어댑터를 사용한다는 것을 알고 쓰지 않았던 뷰였다.

1) 리스트뷰

어댑터뷰의 대표 위젯

복수 개의 항목을 수직으로 표시

 

2) 사용방법

  -> 프로젝트 생성

  -> xml 레이아웃 파일에서 리스트뷰 위젯 정의

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
       />
</LinearLayout>

 

  -> 어댑터 객체 생성

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val items = arrayOf<String?>("item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6",  "item7", "item8")

        //어댑터 준비
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)
   }
}

 

  -> 리스트뷰 객체에 어댑터 연결

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val items = arrayOf<String?>("item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6",  "item7", "item8")

        //어댑터 준비
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)

        // 어댑터를 ListView 객체에 연결
        binding.listView.adapter = adapter
    }
}

 

데이터 원본이 배열인 경우에 ArrayAdapter객체 사용

ArrayAdapter에는 context와 resource, textViewResourceId, objects가 들어가는데 textViewResourceId는 보통 안 들어가는 거 같다.

context는 현재 컨텍스트를 말하고 resource는 항목으로 표시될 텍스트 뷰의 리소스 아이디를 말하며 objects는 어댑터로 공급될 데이터 원본으로 단순 배열 형태이다.

resource에는 하나의 텍스트 뷰로 구성된 레이아웃인 android.R.layout.simple_list_item_1, 두 개의 텍스트 뷰로 구성된 레이아웃인 android.R.layout.simple_list_item_2, 오른쪽에 체크 표시가 나타나는 레이아웃인 android.R.layout.simple_list_item_checked, 오른쪽에 라디오 버튼이 나타나는 레이아웃인 android.R.layout.simple_list_item_single_choice와 오른쪽에 체크 버튼이 나타나는 레이아웃인 android.R.layout.simple_list_item_multiple_choice가 들어갈 수 있다.

 

4. GridView

많이 쓴 걸 본 적이 없는 뷰다.

1) 그리드뷰

어댑터뷰의 대표 위젯

2차원 스크롤 가능한 그리드에 복수 개의 항목을 표시

 

2) 간단한 텍스트 그리드뷰 사용방법

  -> 프로젝트 생성

  -> xml 레이아웃 파일에서 그리드뷰 위젯 정의

<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/gridview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:columnWidth="100dp" <-- 그리드 항목 하나의 폭 설정
    android:numColumns="auto_fit" <-- 열의 너비와 화면의 너비를 바탕으로 자동 계산
    android:verticalSpacing="10dp" <-- 그리드 항목 간의 간격 설정
    android:horizontalSpacing="10dp"
    android:stretchMode="columnWidth" <-- 열 내부의 여백을 너비에 맞게 채움
    android:gravity="center"
    />

  -> ArrayAdapter 객체 생성 후 그리드뷰 객체에 연결

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val items = arrayOf<String?>("item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6",  "item7", "item8")

        //어댑터 준비
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)

        // 어댑터를 GridView 객체에 연결
        binding.gridView.adapter = adapter

    }
}

 

3) 이미지 그리드뷰 사용방법

  -> 프로젝트 생성

  -> xml 레이아웃 파일에서 그리드뷰 위젯 정의

<GridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/gridview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:columnWidth="100dp"
    android:gravity="center"
    android:horizontalSpacing="10dp"
    android:numColumns="auto_fit"
    android:stretchMode="columnWidth"
    android:verticalSpacing="10dp" />

 

  -> 이미지를 사용하고자 하면 ImageAdapter를 BaseAdapter를 상속받아 어댑터 정의

class ImageAdapter : BaseAdapter() {
    // 항목의 총 개수를 반환하기 위해 mThumbIds 배열의 크기 반환
    override fun getCount(): Int {
        return mThumbIds.size
    }
    
    // 특정 위치의 항목을 반환하기 위해 mThumbIds 배열의 지정된 위치의 항목 반환
    override fun getItem(position: Int): Any {
        return mThumbIds[position]
    }

    // 특정 위치의 항목 아이디 반환
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    // 첫번째 파라미터로 주어진 위치의 항목 뷰를 반환하는 것
    // 배열 내 이미지 리소스를 ImageView의 이미지로 설정하고 해당 ImageView 객체를 그리드뷰의 항목뷰로 반환
    // 두번째 파라미터로 주어진 건 이전에 생성된 항목뷰를 의미
    // 항목뷰가 처음 만들어지는 게 아니라면 재사용
    
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val imageView: ImageView
        if (convertView == null) {
            imageView = ImageView(parent!!.context)
            imageView.layoutParams = AbsListView.LayoutParams(200, 200)
            imageView.scaleType = ImageView.ScaleType.CENTER_CROP
            imageView.setPadding(8, 8, 8, 8)
        } else {
            imageView = convertView as ImageView
        }

        imageView.setImageResource(mThumbIds.get(position))
        return imageView
    }

    private val mThumbIds = arrayOf<Int>(
        R.drawable.sample_2, R.drawable.sample_3,
        R.drawable.sample_4, R.drawable.sample_5,
        R.drawable.sample_6, R.drawable.sample_7,
        R.drawable.sample_0, R.drawable.sample_1,
        R.drawable.sample_2, R.drawable.sample_3,
        R.drawable.sample_4, R.drawable.sample_5,
        R.drawable.sample_6, R.drawable.sample_7,
        R.drawable.sample_0, R.drawable.sample_1,
        R.drawable.sample_2, R.drawable.sample_3,
        R.drawable.sample_4, R.drawable.sample_5,
        R.drawable.sample_6, R.drawable.sample_7
    )
}

 

  -> 어댑터 생성 후 그리드뷰 객체에 연결

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // ImageAdapter 객체를 생성하고 GridView 객체에 연결
        binding.gridview.adapter = ImageAdapter()


    }
}

 

-> 항목 클릭 이벤트 처리

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // ImageAdapter 객체를 생성하고 GridView 객체에 연결
        binding.gridview.adapter = ImageAdapter()

        // 항목 클릭 이벤트 처리
        // parent : 클릭 이벤트가 발생된 어댑터뷰
        // view : 실제 클리된 어댑터뷰 안의 뷰
        // position : 어댑터 내에서 클릭된 항목(뷰)의 위치
        // id : 클릭된 항목의 아이디
        binding.gridview.setOnItemClickListener{ parent, view, position, id ->
            Toast.makeText(this@MainActivity,"" + (position + 1) + "번째 선택",           
                Toast.LENGTH_SHORT).show()
        }
    }
}

 

5. RecyclerView

튜터님도 말하길 거의 모든 앱에서 다 사용되는 뷰

자세히 알고 여러번 사용하면서 머리와 손에 완전히 익혀야겠다.

1) 리사이클러뷰

안드로이드 앱에서 리스트 형태의 데이터를 표시하는데 사용되는 위젯

여러 아이템을 스크로 가능한 리스트로 표현

많은 아이템을 효율적으로 관리하고 보여주는 역할

한정적인 화면에 뷰를 재활용하여 많은 데이터를 넣을 수 있는 뷰

 

2) 리스트뷰와의 차이점

리스트뷰는 사용자가 스크롤 할 때마다 위에 있는 아이템은 삭제되고 맨 아래에 아이템은 생성되는 걸 반복하지만 리사이클러뷰는 위에 있는 아이템이 재활용되기 위해 아래로 이동합니다.

리스트뷰는 아이템이 100개면 100개에 대해 계속 삭제와 생성을 반복해 성능에 좋지 않지만 리사이클러뷰는 아이템이 100개여도 10개 정도의 뷰를 가지고 재활용하며 사용해 리스트뷰의 단점을 보완한다.

 

3) 사용에 필요한 것

데이터 테이블을 다양한 형식의 리스트로 보여주기 위해 데이터와 리사이클러뷰 사이에 존재하며 사용되는 것인 어댑터가 필요하다.

간단하게는 데이터와 리사이클러뷰 사이의 통신을 위한 연결체라고 할 수 있다.

 

화면에 표시될 데이터나 아이템들을 저장하는 역할을 하는 뷰홀더가 필요하다.

스크롤해서 위로 올라간 뷰를 재활용하기 위해서는 해당 뷰를 기억하고 있어야하는데 뷰홀더가 그 역할을 한다.

 

4) 사용방법

  -> 프로젝트 생성

  -> xml 레이아웃 파일에서 리사이클러뷰 위젯 정의

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
       />
</LinearLayout>

 

  -> 어댑터 클래스 정의

class MyAdapter(val mItems: MutableList<MyItem>) : RecyclerView.Adapter<MyAdapter.Holder>() {

    // 클릭 이벤트를 처리하기 위한 인터페이스
    interface ItemClick {
        fun onClick(view : View, position : Int)
    }

    var itemClick : ItemClick? = null

    // 뷰홀더를 생성하는 함수
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }
    
    // 뷰홀더와 데이터를 묶는 함수
    override fun onBindViewHolder(holder: Holder, position: Int) {
        holder.itemView.setOnClickListener {  //클릭이벤트추가부분
            itemClick?.onClick(it, position)
        }
        holder.iconImageView.setImageResource(mItems[position].aIcon)
        holder.name.text = mItems[position].aName
        holder.age.text = mItems[position].aAge
    }

    // 특정 위치의 아이템 아이디 값을 반환
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    // 총 데이터 개수 반환
    override fun getItemCount(): Int {
        return mItems.size
    }

    // 리사이클러뷰 아이템 레이아웃을 가지는 뷰홀더 
    inner class Holder(val binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root) {
        val iconImageView = binding.iconItem
        val name = binding.textItem1
        val age = binding.textItem2
    }
}

 

  -> 어댑터 생성 후 리사이클러뷰 객체에 연결 & 레이아웃 매니저 설정

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val dataList = mutableListOf<MyItem>()
        dataList.add(MyItem(R.drawable.sample_0, "Bella", "1"))
        dataList.add(MyItem(R.drawable.sample_1, "Charlie", "2"))
        dataList.add(MyItem(R.drawable.sample_2, "Daisy", "1.5"))
        dataList.add(MyItem(R.drawable.sample_3, "Duke", "1"))
        dataList.add(MyItem(R.drawable.sample_4, "Max", "2"))
        dataList.add(MyItem(R.drawable.sample_5, "Happy", "4"))
        dataList.add(MyItem(R.drawable.sample_6, "Luna", "3"))
        dataList.add(MyItem(R.drawable.sample_7, "Bob", "2"))

        // 어댑터 연결
        val adapter = MyAdapter(dataList)
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
    }
}

 

  -> 아이템 클릭 이벤트 처리

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val dataList = mutableListOf<MyItem>()
        dataList.add(MyItem(R.drawable.sample_0, "Bella", "1"))
        dataList.add(MyItem(R.drawable.sample_1, "Charlie", "2"))
        dataList.add(MyItem(R.drawable.sample_2, "Daisy", "1.5"))
        dataList.add(MyItem(R.drawable.sample_3, "Duke", "1"))
        dataList.add(MyItem(R.drawable.sample_4, "Max", "2"))
        dataList.add(MyItem(R.drawable.sample_5, "Happy", "4"))
        dataList.add(MyItem(R.drawable.sample_6, "Luna", "3"))
        dataList.add(MyItem(R.drawable.sample_7, "Bob", "2"))

        // 어댑터 연결
        val adapter = MyAdapter(dataList)
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        
        // 어댑터 내의 인터페이스를 오버라이딩하여 아이템 클릭 이벤트 처리
        adapter.itemClick = object : MyAdapter.ItemClick {
            override fun onClick(view: View, position: Int) {
                val name: String = dataList[position].aName
                Toast.makeText(this@MainActivity," $name 선택!", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

 

6. Fragment

1) 프래그먼트

액티비티와 분리되어 독립적으로 동작할 수 없으며 액티비티 위에서 동작하는 모듈화된 사용자 인터페이스
안드로이드 애플리케이션의 UI 부분을 모듈화하여 재사용할 수 있도록 해주는 구성 요소

여러 개의 프래그먼트를 하나의 액티비티에 조합하여 창이 여러 개인 UI를 구축할 수 있으며,하나의 프래그먼트를 여러 액티비티에서 재사용할 수 있음

 

2) 액티비티와 프래그먼트

액티비티는 시스템의 액티비티 매니저에서 인텐트를 해석해 액티비티 간에 데이터를 전달하지만 프래그먼트는 액티비티의 프래그먼트 매니저에서 메소드로 프래그먼트 간에 데이터를 전달한다.

 

3) 사용이유

액티비티로 화면을 계속 넘기는 것보다 프래그먼트로 일부만 바꾸는 것이 자원 이용량이 적어 속도가 빠르다.

액티비티를 적게 만들면서도 여러 화면을 보여줄 수 있다.

액티비티의 복잡도를 줄일 수 있다.

재사용 가능한 레이아웃을 분리해서 관리가 가능하다.

 

4) 프래그먼트 생명주기

프래그먼트는 자체적인 생명주기(lifecycle)를 가지며, 액티비티의 생명주기와 밀접하게 연결되어 있다.

관련된 정리는 https://retry-thinksubox.tistory.com/91 에 있다.

 

5) 프래그먼트 정적 추가 & 동적 추가

정적으로 추가하는 방법은 액티비티의 레이아웃 파일 안에서 선언하는 것이다.

각 프래그먼트에는 액티비티가 재시작되는 경우 프래그먼트를 복구하기 위해 시스템이 사용할 수 있는 고유한 식별자가 필요한데 android:id 속성을 제공하거나 android:tag 속성을 제공하거나 둘 다 제공하지 않으면 시스템이 알아서 컨테이너뷰의 ID를 사용한다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
    <fragment
        android:name="com.skmns.fragmentbasic.FirstFragment" <-- 레이아웃 안에서 인스턴스화할 프래그먼트 클래스 지정
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/fragment" /> <-- 고유 식별자 제공
</LinearLayout>

 

동적으로 추가하는 방법은 코틀린 코드 상에서 supportFragmentManager를 사용하는 것이다.

supportFragmentManager는 사용자 상호작용에 응답해 Fragment를 추가하거나 삭제하는 등의 작업을 할 수 있게 해주는 매니저다.

supportFragmentManager.commit {
    // 어느 프레임 레이아웃에 어떤 프래그먼트를 띄울 것인가
    replace(R.id.frameLayout, frag)
    // 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화
    setReorderingAllowed(true)
    // 뒤로가기 버튼 클릭시 다음 액션 설정
    addToBackStack("")
}

 

6) 사용방법

  -> 프로젝트 생성

  -> 빈 프래그먼트 생성

  -> 프래그먼트 xml 레이아웃 파일 수정

  -> 액티비티 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 프래그먼트가 들어갈 프레임 레이아웃 -->
    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        app:layout_constraintBottom_toTopOf="@+id/fragment1_btn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </FrameLayout>

    <Button
        android:id="@+id/fragment1_btn"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="Frag1"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/fragment2_btn"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/frameLayout" />

    <Button
        android:id="@+id/fragment2_btn"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="Frag2"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/fragment1_btn"
        app:layout_constraintTop_toBottomOf="@+id/frameLayout" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

  -> 메인 액티비티에서 프래그먼트 추가

class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        binding.apply {
            fragment1Btn.setOnClickListener{
                setFragment(FirstFragment())
            }
            fragment2Btn.setOnClickListener {
                setFragment(SecondFragment())
            }
        }
        setFragment(FirstFragment())
    }

    private fun setFragment(frag : Fragment) {
        supportFragmentManager.commit {
            replace(R.id.frameLayout, frag)
            setReorderingAllowed(true)
            addToBackStack("")
        }
    }
}

 

7. 프래그먼트의 데이터 전달

1) 액티비티 -> 프래그먼트

프래그먼트의 인스턴스를 생성하고 newInstance 메소드를 통해 데이터를 전달

Bundle 객체를 사용하여 데이터를 프래그먼트의 arguments로 설정하고 이걸 프래그먼트가 받아 사용

// MainActivity.kt

binding.run {
    fragment1Btn.setOnClickListener{
        // [1] Activity -> FirstFragment
        val dataToSend = "Hello First Fragment! \n From Activity"
        // 보낼 데이터와 함께 프래그먼트 인스턴스를 생성
        val fragment = FirstFragment.newInstance(dataToSend)
        setFragment(fragment)
    }

    fragment2Btn.setOnClickListener {
        // [1] Activity -> SecondFragment
        val dataToSend = "Hello Second Fragment!\n From Activity"
        // 보낼 데이터와 함께 프래그먼트 인스턴스를 생성
        val fragment = SecondFragment.newInstance(dataToSend)
        setFragment(fragment)
    }
}
// FirstFragment.kt

private const val ARG_PARAM1 = "param1"
private var param1: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // arguments 값이 null이 아닐 때, param1에 해당 데이터(문자열)를 넣어줌
    arguments?.let {
        param1 = it.getString(ARG_PARAM1)
    }
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // [1] Activity -> FirstFragment
        binding.tvFrag1Text.text = param1     
}

companion object {
    @JvmStatic
    fun newInstance(param1: String) =
        // [1] Activity -> FirstFragment
        FirstFragment().apply {
            // arguments가 액티비티에서 보낸 데이터가 들어있는 Bundle 객체를 받음
            arguments = Bundle().apply {
                putString(ARG_PARAM1, param1)
        }
    }
}

 

2) 프래그먼트 -> 프래그먼트

첫 번째 프래그먼트에서 두 번째 프래그먼트의 newInstance 메소드를 사용하여 인스턴스를 생성하고 데이터를 전달

// FirstFragment.kt

// [2] Fragment -> Fragment
binding.btnGofrag2.setOnClickListener{
    val dataToSend = "Hello Fragment2! \n From Fragment1"
    // 보낼 데이터와 함께 프래그먼트 인스턴스를 생성
    val fragment2 = SecondFragment.newInstance(dataToSend)
    // 프래그먼트 트랜잭션을 통해 프래그먼트 시작
    requireActivity().supportFragmentManager.beginTransaction()
        .replace(R.id.frameLayout, fragment2)
        .addToBackStack(null)
        .commit()
}
// SecondFragment.kt

private const val ARG_PARAM1 = "param1"

class SecondFragment : Fragment() {

    private var param1: String? = null

    private var _binding: FragmentSecondBinding? = null
    private val binding get() = _binding!!


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // arguments 값이 null이 아닐 때, param1에 해당 데이터(문자열)를 넣어줌
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
        }
    }

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // [2] Fragment -> Fragment
        binding.tvFrag2Text.text = param1
    }


    companion object {
        @JvmStatic
        fun newInstance(param1: String) =
            // [1] Activity -> FirstFragment
            SecondFragment().apply {
                // arguments가 액티비티에서 보낸 데이터가 들어있는 Bundle 객체를 받음
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                }
            }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        // Binding 객체 해제
        _binding = null
    }
}

 

3) 프래그먼트 -> 액티비티

먼저 콜백 인터페이스 정의하고 해당 인터페이스를 액티비티가 상속받아 구현

프래그먼트는 이 인터페이스를 사용해 액티비티에 데이터를 전달

// SecondFragment.kt

private const val ARG_PARAM1 = "param1"

// 인터페이스 정의
interface FragmentDataListener {
    fun onDataReceived(data: String)
}

class SecondFragment : Fragment() {

    // [3] SecondFragment -> Activity
    // 인터페이스를 타입으로 가지는 리스너 변수 생성
    private var listener: FragmentDataListener? = null

    private var param1: String? = null

    private var _binding: FragmentSecondBinding? = null
    private val binding get() = _binding!!


    override fun onAttach(context: Context) {
        super.onAttach(context)

        // [3] SecondFragment -> Activity
        // 호스트 액티비티에 인터페이스가 구현되어있는지 확인
        if (context is FragmentDataListener) {
            // 구현되어있다면 리스너를 할당
            listener = context
        } else {
            throw RuntimeException("$context must implement FragmentDataListener")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
        }
    }

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // [2] Fragment -> Fragment
        binding.tvFrag2Text.text = param1

        // [3] SecondFragment -> Activity
        binding.btnSendActivity.setOnClickListener{
            val dataToSend = "Hello from SecondFragment!"
            // 할당된 리스너의 메서드를 호출하여 프래그먼트에서 액티비티로 데이터 전달
            listener?.onDataReceived(dataToSend)
        }
    }


    companion object {
        @JvmStatic
        fun newInstance(param1: String) =
            // [1] Activity -> FirstFragment
            SecondFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                }
            }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        // Binding 객체 해제
        _binding = null
        listener = null
    }
}
// MainActivity.kt

// FragmentDataListener 인터페이스를 상속
class MainActivity : AppCompatActivity(), FragmentDataListener {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        binding.run {
            fragment1Btn.setOnClickListener{
                // [1] Activity -> FirstFragment
                val dataToSend = "Hello First Fragment! \n From Activity"
                val fragment = FirstFragment.newInstance(dataToSend)
                setFragment(fragment)
            }

            fragment2Btn.setOnClickListener {
                // [1] Activity -> SecondFragment
                val dataToSend = "Hello Second Fragment!\n From Activity"
                val fragment = SecondFragment.newInstance(dataToSend)
                setFragment(fragment)
            }
        }

        setFragment(FirstFragment())
    }

    private fun setFragment(frag : Fragment) {
        supportFragmentManager.commit {
            replace(R.id.frameLayout, frag)
            setReorderingAllowed(true)
            addToBackStack("")
        }
    }

    // [3] SecondFragment -> Activity
    // 상속받은 인터페이스의 메소드를 오버라이딩
    override fun onDataReceived(data: String) {
        // Fragment에서 받은 데이터를 처리
        Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
    }
}

 

8. 다이얼로그

1) 소개

사용자에게 결정을 내리거나 추가정보를 입력하라는 메시지를 표시하는 작은 창

화면을 가득 채우지 않음

사용자가 앱 내에서 계속 진행하기 전에 조치를 취해야 하는 모달 이벤트에 사용

 

2) 구조

제목 영역

  - 선택사항

  - 콘텐츠 영역에 상세한 메시지,목록 또는 맞춤 레이아웃이 채워져 있는 경우에만 사용

  - 단순한 메시지 또는 질문을 나타내야 하는 경우 제목은 없어도 됨

콘텐츠 영역

  - 메시지,목록 또는 다른 맞춤 레이아웃 표시 가능

작업 버튼 영역

  - 최대 버튼 개수는 3개

 

3) 기본 다이얼로그

binding.btn1Alert.setOnClickListener {
    var builder = AlertDialog.Builder(this)
    builder.setTitle("기본 다이얼로그 타이틀")
    builder.setMessage("기본 다이얼로그 메세지")
    builder.setIcon(R.mipmap.ic_launcher)

    // 버튼 클릭시에 수행할 작업 설정
    val listener = object : DialogInterface.OnClickListener {
        override fun onClick(p0: DialogInterface?, p1: Int) {
            when (p1) {
                DialogInterface.BUTTON_POSITIVE ->
                    binding.tvTitle.text = "BUTTON_POSITIVE"
                DialogInterface.BUTTON_NEUTRAL ->
                    binding.tvTitle.text = "BUTTON_NEUTRAL"
                DialogInterface.BUTTON_NEGATIVE ->
                    binding.tvTitle.text = "BUTTON_NEGATIVE"
            }
        }
    }

    // 작업 버튼 설정
    builder.setPositiveButton("Positive", listener)
    builder.setNegativeButton("Negative", listener)
    builder.setNeutralButton("Neutral", listener)

    builder.show()
}

 

4) 커스텀 다이얼로그

binding.btn2Custom.setOnClickListener {
    val builder = AlertDialog.Builder(this)
    builder.setTitle("커스텀 다이얼로그")
    builder.setIcon(R.mipmap.ic_launcher)
    
    // 콘텐츠 영역에 들어갈 레이아웃을 만들어서 집어넣음
    // 레이아웃 파일을 만들어놔야함
    val v1 = layoutInflater.inflate(R.layout.dialog, null)
    builder.setView(v1)

    // p0에 해당 AlertDialog가 들어온다. findViewById를 통해 view를 가져와서 사용
    // 기본 다이얼로그에서는 onClick을 오버라이딩했음
    val listener = DialogInterface.OnClickListener { p0, p1 ->
        val alert = p0 as AlertDialog
        val edit1: EditText? = alert.findViewById<EditText>(R.id.editText)
        val edit2: EditText? = alert.findViewById<EditText>(R.id.editText2)

        binding.tvTitle.text = "이름 : ${edit1?.text}"
        binding.tvTitle.append(" / 나이 : ${edit2?.text}")
    }

    // 작업 버튼 설정
    builder.setPositiveButton("확인", listener)
    builder.setNegativeButton("취소", null)

    builder.show()
}

 

5) 날짜 다이얼로그

binding.btn3Date.setOnClickListener {
    // 오늘 날짜를 가져옴
    val calendar = Calendar.getInstance()
    val year = calendar.get(Calendar.YEAR)
    val month = calendar.get(Calendar.MONTH)
    val day = calendar.get(Calendar.DAY_OF_MONTH)

    // 다른 날짜를 선택한 경우
    val listener = DatePickerDialog.OnDateSetListener { datePicker, year, month, day ->
        // year년 month월 day일
        binding.tvTitle.text = "${year}년 ${month + 1}월 ${day}일"
    }
    
    // 처음 세팅을 오늘 날짜로
    var picker = DatePickerDialog(this, listener, year, month, day)
    picker.show()
}

 

6) 시간 다이얼로그

binding.btn4Time.setOnClickListener {
    // 현재 시간을 가져옴
    val calendar = Calendar.getInstance()
    val hour = calendar.get(Calendar.HOUR)
    val minute = calendar.get(Calendar.MINUTE)

    // 다른 시간을 선택한 경우
    val listener = TimePickerDialog.OnTimeSetListener { timePicker, hour, minute ->
        binding.tvTitle.text = "${hour}시 ${minute}분"
    }
    
    // 처음 세팅을 현재시간으로
    // 마지막 매개변수를 true로 하면 시간을 24시간제로 표시
    val picker = TimePickerDialog(this, listener, hour, minute, false) 
    picker.show()
}

 

7) 진행 다이얼로그

binding.btn5Porgress.setOnClickListener {
    // 커스텀 다이얼로그와 비슷
    val builder = AlertDialog.Builder(this)
    builder.setTitle("프로그래스바")
    builder.setIcon(R.mipmap.ic_launcher)
    
    // 미리 프로그레스바가 들어가있는 레이아웃 파일을 만들어 집어넣기
    val v1 = layoutInflater.inflate(R.layout.progressbar, null)
    builder.setView(v1)

    builder.show()
}

 

8) 다이얼로그 프래그먼트

대화상자 형태의 UI를 구현하고 관리할 때 사용하는 클래스

Fragment 클래스를 상속받아 모든 기능을 제공

대화상자의 생명주기를 더욱 세밀하게 관리할 수 있으며, 화면 회전 같은 구성 변경 시에도 대화상자의 상태를 유지 가능

전통적인 대화상자, 부모 액티비티의 컨텍스트 내에서 실행되는 내장된 프래그먼트, 대화상자와 유사한 UI를 가진 전체 화면 프래그먼트에 사용됨

class MyDialogFragment : DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let {
            val builder = AlertDialog.Builder(it)
            builder.setMessage("Do you like this app?")
                .setPositiveButton("Yes") { dialog, id ->
                    // Yes를 눌렀을 때 호스트 액티비티로 보낼 이벤트
                }
                .setNegativeButton("No") { dialog, id ->
                    // No를 눌렀을 때 호스트 액티비티로 보낼 이벤트
                }
            builder.create()
        } ?: throw IllegalStateException("Activity cannot be null")
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.showDialogButton).setOnClickListener {
            MyDialogFragment().show(supportFragmentManager, "MyDialogFragment")
        }
    }
}

 

9. 알림

1) 소개

앱의 UI와 별도로 사용자에게 앱과 관련한 정보를 보여주는 기능

알림을 터치하여 해당 앱을 열 수 있음

보통 단말기 상단 부분에 표시되고, 앱 아이콘의 배지로도 표시

 

2) 알림 채널(Android 8.0이상)

Android 8.0이상의 경우는 알림을 만들기 전에 알림 채널을 먼저 만들어야함

알림 채널은 알림을 그룹하여 알림 활성화나 방식을 변경 가능

private val myNotificationID = 1
private val channelID = "default"

private fun createNotificationChannel() {
    // Android 8.0 이상인지 확인
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 
        val channel = NotificationChannel(channelID, "default channel",
            NotificationManager.IMPORTANCE_DEFAULT)
        channel.description = "description text of this channel."
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }
}

 

3) 알림 생성

private val myNotificationID = 1

private fun showNotification() {
    // NotificationCompat.Builder 객체에서 알림에 대한 UI정보와 작업을 지정
    val builder = NotificationCompat.Builder(this, channelID)
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("title")
        .setContentText("notification text")
        // 알림 우선순위(Android 7.1 이하)
        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
    // Notification 객체를 반환하는 NotificationCompat.Builder.build()호출
    // NotificationManagerCompat.notify()를 호출해서 시스템에 Notification객체를 전달
    NotificationManagerCompat.from(this).notify(myNotificationID, builder.build())
}

 

4) 알림 중요도

 

5) 알림 확장뷰

  - 긴 텍스트

builder.setStyle(NotificationCompat.BigTextStyle()
                .bigText(resources.getString(R.string.long_notification_body)))

 

  - 이미지 추가

val bitmap = BitmapFactory.decodeResource(resources, R.drawable.<리소스 이름>)
val builder = NotificationCompat.Builder(this, channelID)
    .setSmallIcon(R.mipmap.ic_launcher)
    .setLargeIcon(bitmap)
    .setContentTitle("Notification Title")
    .setContentText("Notification body")
    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
    .setStyle(NotificationCompat.BigPictureStyle()
        .bigPicture(bitmap)
        .bigLargeIcon(null))

 

  - 버튼 추가

val intnet = Intent(this, TestActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val builder = NotificationCompat.Builder(this, channelID)
    .setSmallIcon(R.mipmap.ic_launcher)
    .setContentTitle("Notification Title")
    .setContentText("Notification body")
    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
    .addAction(R.drawable.<리소스 이름>, "Action", pendingIntent)
NotificationManagerCompat.from(this).notify(myNotificationID, builder.build())

 

  - 프로그레스바 추가

val builder = NotificationCompat.Builder(this, channelID)
    .setSmallIcon(R.mipmap.ic_launcher)
    .setContentTitle("Progress")
    .setContentText("In progress")
    .setProgress(100, 0, false)
    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
NotificationManagerCompat.from(this).notify(myNotificationID, builder.build())

Thread {
    for (i in (1..100).step(10) {
        Thread.sleep(1000)
        builder.setProgress(100, i, false)
        NotificationManagerCompat.from(this).notify(myNotificationID, builder.build())
    }
    builder.setContentText("Completed").setProgress(0, 0, false)
    NotificationManagerCompat.from(this).notify(myNotificationID, builder.build())
}.start()

 

6) 알림에 액티비티 연결

// AndroidManifest.xml
<activity android:name=".SecondActivity" android:parentActivityName=".MainActivity" />

val intent = Intent(this, SecondActivity::class.java)
val pendingIntent = with (TaskStackBuilder.create(this)) {
    addNextIntentWithParentStack(intent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
val builder = NotificationCompat.Builder(this, channelID)
    .setSmallIcon(R.mipmap.ic_launcher)
    .setContentTitle("Notification Title")
    .setContentText("Notification body")
    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
    .setContentIntent(pendingIntent)
    .setAutoCancel(true) // 알림 클릭하면 자동으로 없어지는 것
NotificationManagerCompat.from(this).notify(myNotificationID, builder.build())

알림을 터치하면 SecondActivity가 실행되고 뒤로가면 MainActivity가 나옴

이건 MainActivity가 이미 백스택에 있기 때문에 백스택을 조작하지 않아도 MainActivity가 나오는데 백스택에 없는 다른 액티비티를 SecondActivity의 parentActivity로 하면 다른 결과가 나옴

 

7) 예제

<!-- activity_main.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/notificationButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="알림 보내기"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        binding.notificationButton.setOnClickListener{
            notification()
        }
    }

    fun notification(){
        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager

        val builder: NotificationCompat.Builder
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            // 26 버전 이상
            val channelId="one-channel"
            val channelName="My Channel One"
            val channel = NotificationChannel(
                channelId,
                channelName,
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply {
                // 채널에 다양한 정보 설정
                description = "My Channel One Description"
                setShowBadge(true)
                val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
                val audioAttributes = AudioAttributes.Builder()
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .setUsage(AudioAttributes.USAGE_ALARM)
                    .build()
                setSound(uri, audioAttributes)
                enableVibration(true)
            }
            // 채널을 NotificationManager에 등록
            manager.createNotificationChannel(channel)

            // 채널을 이용하여 builder 생성
            builder = NotificationCompat.Builder(this, channelId)

        }else {
            // 26 버전 이하
            builder = NotificationCompat.Builder(this)
        }

		val bitmap = BitmapFactory.decodeResource(resources, R.drawable.flower)
        val intent = Intent(this, SecondActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
        // 알림의 기본 정보
        builder.run {
            setSmallIcon(R.mipmap.ic_launcher)
            setWhen(System.currentTimeMillis())
            setContentTitle("새로운 알림입니다.")
            setContentText("알림이 잘 보이시나요.")
            setStyle(NotificationCompat.BigTextStyle()
                .bigText("이것은 긴텍스트 샘플입니다. 아주 긴 텍스트를 쓸때는 여기다 하면 됩니다.이것은 긴텍스트 샘플입니다. 
아주 긴 텍스트를 쓸때는 여기다 하면 됩니다.이것은 긴텍스트 샘플입니다. 아주 긴 텍스트를 쓸때는 여기다 하면 됩니다."))
            setLargeIcon(bitmap)
//            setStyle(NotificationCompat.BigPictureStyle()
//                    .bigPicture(bitmap)
//                    .bigLargeIcon(null))  // hide largeIcon while expanding
            addAction(R.mipmap.ic_launcher, "Action", pendingIntent)
            setAutoCancel(true)
        }

        manager.notify(11, builder.build())
    }
}

 

8) Android API 33 이상에서 권한 시스템 변경

개발자는 알림을 보내기 위해 POST_NOTIFICATIONS 권한을 앱의 매니페스트 파일에 명시적으로 추가해야함

이것으로 사용자의 프라이버시를 강화하고, 앱이 사용자의 동의 없이 알림을 보내는 것을 방지할 수 있음

사용자는 앱 설치 후 처음 알림을 수신할 때 이 권한을 부여할지 결정하는 대화상자를 볼 수 있음

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <!-- API 33 이상을 위한 알림 권한 추가 -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

    ...
</manifest>

 

앱이 실행 중일 때 사용자에게 알림 권한을 요청하는 방법은 API 33 이상인지 확인하고 알림이 활성화되어 있는지를 확인한 다음에 권한이 없을 경우 설정 화면으로 사용자를 안내하는 것

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) {
        // 알림 권한이 없다면, 사용자에게 권한 요청
        val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
            putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
        }
        startActivity(intent)
    }
}

 

알림 권한 요청은 사용자 경험을 고려하여 적절한 시점에 수행해야 함

또한 사용자가 알림의 가치를 이해할 수 있도록 설득력 있는 메시지를 제공해야 함

반응형