안드로이드 앱 개발 공부/자꾸 까먹어서 적어두는 구현방법

[Android] App widget 만들기 _ configureActivity + recyclerview

플래시🦥 2023. 4. 27.
반응형

이전에 jetpack compose로 만든 앱에 위젯 추가 기능을 추가하려고 했는데, 아직 jetpack compose로는 이전처럼 모든 기능을 구현할 수 없다는 글을 보고 열심히 분석하며 만들어 보려고 했던 것을 포기했었다. 

 

그래서 기존방법으로 다시 만들어 보았다. 

이 앱은 jetpack compose를 한번 사용해 보고 싶어서 간단한 앱을 목표로 시작했고 플레이 스토어에 업로드까지 하게 되었다. 이 앱은 투두, 메모, 디데이를 하나의 앱에서 간단하게 관리할 수 있는 앱이다. 

그래서 homescreen 위젯을 추가해서 볼 수 있었으면 했다. 

 

내가 구현하려는 기능은 위젯을 추가하여 사용자가 저장해 두었던 디데이를 Room Db에서 가져와 RecyclerView를 통해 리스트로 보여주고 그 중 원하는 아이템의 라디오버튼을 클릭하여 추가버튼을 누르면 위젯이 추가되는 것이다. 

 

1. 우선 앱을 만들 때 액티비티 추가하듯 자신의 패키지를 우클릭 해서 위젯을 추가하는 방법을 추천한다. 

원하는 세팅을 해서 finish 하면 된다. 

그러면 여러 파일들이 자동으로 추가가 될 것이다. 그리고 예제 코드도 들어가 있어서 처음 공부해 보고 감을 잘 못 잡겠다 하는 사람들에게 좋은 길잡이가 되어준다. 나도 그렇게 해서 위젯추가에 성공했다. 

 

이 방법을 사용하면 다른 블로그 글을 서치해보지 않아도 스스로 분석해서 원하는 기능으로 변경하여 사용할 수 있다. 

 

여기서 추가된 파일들 중에 중요한 것은 (클래스 이름을 NewAppWidget으로 했다는 것을 기준으로)

NewAppWidget.kt

NewAppWidgetConfigureActivity.kt

new_app_widget.xml

new_app_widget_configure.xml

new_app_widget_info.xml 이다. 

manifest에 receiver를 등록해야 하지만 위 방식으로 파일을 추가할 경우 자동으로 추가되기 때문에 생략했다. 

 

각 파일의 기능은 설명하자면 대략 아래와 같다. 

NewAppWidget.kt  // 앱위젯의 기능적 구현_ 업데이트, 삭제 등 

NewAppWidgetConfigureActivity.kt //앱위젯의 설정 스크린 

new_app_widget.xml //NewAppWidget.kt Ui

new_app_widget_configure.xml //NewAppWidgetConfigureActivity.kt Ui

new_app_widget_info.xml //앱위젯 기본 설정 

 

추가적으로 리사이클러뷰를 위한 어뎁터와 아이템 xml 파일도 필요하다. 

*룸디비 코드와 xml코드는 생략 (코틀린 코드보고 파악가능함)

2. 리사이클러 어댑터

package com.example.allmyreview

import android.annotation.SuppressLint
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.recyclerview.widget.RecyclerView
import com.jaysdevapp.stickymemory.calDate
import com.jaysdevapp.stickymemory.databinding.RecyclerDdayBinding
import com.jaysdevapp.stickymemory.dataclasses.Dday


class myDDayAdapter(private var data: ArrayList<Dday>) :
    RecyclerView.Adapter<myDDayAdapter.MyViewHolder>() {

    val TAG = "myDDayAdapter"
    private var checkedRadioButton: CompoundButton? = null
    var checkId = -1	//체크된 라디오 버튼
    @SuppressLint("NotifyDataSetChanged")
    fun update(newDatas: List<Dday>) {
        data.clear()
        data.addAll(newDatas)
        notifyDataSetChanged()
    }
    // 생성된 뷰 홀더에 값 지정
    inner class MyViewHolder(val binding: RecyclerDdayBinding) :
        RecyclerView.ViewHolder(binding.root) {
        private val context = binding.root.context

        @SuppressLint("SetTextI18n")
        fun bind(datas: Dday,position: Int) {
            binding.ddayNm.text=datas.ddayThing
            binding.ddayDate.text= calDate(datas.date)

            checkedRadioButton = binding.radioButton
			//라디오 버튼 한개만 클릭되도록 이 코드 없으면 리사이클러의 다수의 라디오 버튼이 클릭됨 
            binding.radioButton.setOnCheckedChangeListener { buttonView, isChecked ->
                checkedRadioButton?.apply { setChecked(!isChecked) }
                checkedRadioButton = buttonView.apply { setChecked(isChecked) }
                checkId = position
            }

        }

    }

    // 어떤 xml 으로 뷰 홀더를 생성할지 지정
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        //val binding = RecyclerMovieBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        val inflater = LayoutInflater.from(parent.context)
        val binding = RecyclerDdayBinding.inflate(inflater, parent, false)
        return MyViewHolder(binding)
    }

    // 뷰 홀더에 데이터 바인딩
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val ddays = data
        holder.bind(ddays.get(position),position)

    }

    // 뷰 홀더의 개수 리턴
    override fun getItemCount(): Int {
        return data.size
    }
	//클릭된 라디오 버튼 아이디 리턴
    fun getRadioId() : Int{
        return checkId
    }
	//클릭된 아이템이 가진 데이터 리턴
    fun getcheckData() : Dday{
        return data.get(getRadioId())
    }


}

 

3.  앱 위젯 설정 스크린 기능 

이 부분은 설명하자면 홈스크린에서 위젯을 추가했을 때 바로 위젯이 어떤 형태를 띠는 것이 아니라 사용자가 어떤 부분을 위젯에 보이도록 설정할 것인가를 설정하는 액티비티를 구현하는 것이다. 

이곳에는 이 액티비티의 기능을 구현하는 부분과 그 선택한 데이터를 저장, 삭제, 가져올 수 있는 함수가 존재한다. 

 

액티비티 부분에서 필요한 것은 

1. 룸디비에 있는 데이터 가져오기

2. 가져온 데이터 리사이클러뷰에 넣기

3. 추가 버튼 리스너 

 

간단하게 주석을 붙여서 설명해 보았다. 

나도 아직은 일부 코드는 정확히 어떤 기능을 하는지 설명할 수 는 없으나 기존에 붙어있던 주석으로 그 기능을 파악했다. 

class NewAppWidgetConfigureActivity : Activity() {
    private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
    private var onClickListener = View.OnClickListener {    //add widget 버튼 리스너
        val context = this@NewAppWidgetConfigureActivity
        if(ddayAdapter.checkId!=-1){	//라디오 버튼을 선택 했을 때만 진행
        	
        	saveTitlePref(context, appWidgetId, ddayAdapter.getcheckData()) //디비에서 가져온 , 그중 선택받은 데이터 저장
	       // It is the responsibility of the configuration activity to update the app widget
       		val appWidgetManager = AppWidgetManager.getInstance(context)
	        updateAppWidget(context, appWidgetManager, appWidgetId)
			// Make sure we pass back the original appWidgetId
    	    val resultValue = Intent()
        	resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
	        setResult(RESULT_OK, resultValue)
	        finish()
        }
    }
    
    private lateinit var binding: NewAppWidgetConfigureBinding
    private var ddayAdapter = myDDayAdapter(arrayListOf())	//리라이클러 어뎁터
    
    @SuppressLint("NotifyDataSetChanged")
    public override fun onCreate(icicle: Bundle?) {
        super.onCreate(icicle)

        // Set the result to CANCELED.  This will cause the widget host to cancel
        // out of the widget placement if the user presses the back button.
        setResult(RESULT_CANCELED)

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

        binding.addButton.setOnClickListener(onClickListener)	//클릭리스너 등록
        
        binding.ddayRecycler.apply {	//리사이클러 어뎁터 등록
            layoutManager = LinearLayoutManager(context).also {
                it.orientation = LinearLayoutManager.VERTICAL
            }
            adapter = ddayAdapter
        }
		
        //룸디비에서 데이터 가져와 
        val repository: MutableLiveData<DdayRepository> = MutableLiveData()
        val ddayDao = AppDatabase_dday.getDatabase(this)!!.ddayDao()
        repository.value = DdayRepository(ddayDao)
        var readAllData = repository.value!!.readAllData
		//가져와 저장하려는 변수 관찰
        readAllData.observe(ProcessLifecycleOwner.get(), Observer {
            readAllData.value?.let {
                ddayAdapter.update(it)	//가져왔으면 어뎁터에 보내주기
                ddayAdapter.notifyDataSetChanged()
            }

        })

        // Find the widget id from the intent.
        val intent = intent
        val extras = intent.extras
        if (extras != null) {
            appWidgetId = extras.getInt(
                AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
            )
        }

        // If this activity was started with an intent without an app widget ID, finish with an error.
        if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            finish()
            return
        }

    }

}

뷰모델사용해서 구현해보려 했지만 viewModelprovider에 필요한 ViewModelStoreOwner에 어떤 것을 넣어야 할지 몰라서 뷰모델에 있는 디비 가져오는 코드를 그냥 가져와서 observe 해버렸다.

 

그리고 데이터 저장, 로드, 삭제 함수이다. 

sharedpreference 저장, 로드 삭제 함수이다. 

디데이 이름이랑 날짜랑 setString으로 한 번에 관리하려고 했는데, 그렇게 했더니 set이 오름차순 정렬해서 값을 가지고 있는 메서드라서 디데이 이름이 숫자일 경우 두 값이 섞여서 날짜 계산을 이름으로 넘겨버리는 문제가 있어서 따로 관리하게 되었다. 

// Write the prefix to the SharedPreferences object for this widget
internal fun saveTitlePref(context: Context, appWidgetId: Int, dday: Dday) {
    val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit()
//    prefs.putString(PREF_PREFIX_KEY + appWidgetId, text)

    prefs.putString(PREF_PREFIX_KEY + appWidgetId + "date", dday.date)
    prefs.putString(PREF_PREFIX_KEY + appWidgetId + "name", dday.ddayThing)
    prefs.apply()
}

// Read the prefix from the SharedPreferences object for this widget.
// If there is no preference saved, get the default from a resource
internal fun loadTitlePref_date(context: Context, appWidgetId: Int): String {
    val prefs = context.getSharedPreferences(PREFS_NAME, 0)
    val dateValue = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "date", null)
    return dateValue ?: ""
}

internal fun loadTitlePref_name(context: Context, appWidgetId: Int): String {
    val prefs = context.getSharedPreferences(PREFS_NAME, 0)
    val nameValue = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "name", null)
    return nameValue ?: "none"
}


internal fun deleteTitlePref(context: Context, appWidgetId: Int) {
    val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit()
    prefs.remove(PREF_PREFIX_KEY + appWidgetId)
    prefs.apply()
}

 

4. 저장한 값을 load 하여 위젯 업데이트 

이번에는 NewAppWidget.kt에서 앞서 저장한 데이터를 가지고 위젯 ui에 적용하도록 하는 것이다. 

class NewAppWidget : AppWidgetProvider() {
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // There may be multiple widgets active, so update all of them
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

    override fun onDeleted(context: Context, appWidgetIds: IntArray) {
        // When the user deletes the widget, delete the preference associated with it.
        for (appWidgetId in appWidgetIds) {
            deleteTitlePref(context, appWidgetId)
        }
    }

    override fun onEnabled(context: Context) {
        // Enter relevant functionality for when the first widget is created
    }

    override fun onDisabled(context: Context) {
        // Enter relevant functionality for when the last widget is disabled
    }
}

@RequiresApi(Build.VERSION_CODES.S)
internal fun
        updateAppWidget(   //여기서 ui업데이트
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetId: Int
) {
    val dateText = loadTitlePref_date(context, appWidgetId)
    val nameText = loadTitlePref_name(context, appWidgetId)

    val views = RemoteViews(context.packageName, R.layout.new_app_widget)   //Ui remote하고
    // Construct the RemoteViews object

    if(!dateText.isNullOrBlank()){
        views.setTextViewText(R.id.appwidget_text_nm, nameText)
        views.setTextViewText(R.id.appwidget_text_date, calDate(dateText) )

    }

    // Instruct the widget manager to update the widget
    appWidgetManager.updateAppWidget(appWidgetId, views)
}

*calDate()는 디데이 계산을 위한 함수로 코드 생략

 

이렇게 하면 끝난다. 

 

728x90
반응형

댓글