Summary를 지원하는 Notification 예제

|

NotificationHandler.kt

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.support.v4.app.NotificationCompat
import com.snowdeer.neverfi.R

class NotificationHandler {
    private lateinit var ctx: Context
    private lateinit var notificationManager: NotificationManager

    companion object {
        private const val CHANNEL_ID = "com.snowdeer.neverfi"
        private const val SUMMARY_NOTI_ID = 10003
        private const val NOTIFICATION_GROUP_KEY = "snowdeer_noti"
        val instance = NotificationHandler()
    }

    fun init(ctx: Context) {
        this.ctx = ctx
        notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    }

    fun createNotificationChannel(name: String, description: String) {
        val importance = NotificationManager.IMPORTANCE_LOW
        val channel = NotificationChannel(CHANNEL_ID, name, importance)

        channel.description = description
        channel.enableLights(true)
        channel.lightColor = Color.RED
        channel.enableVibration(true)
        channel.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
        notificationManager.createNotificationChannel(channel)
    }

    fun showNotification(id: Int, text: String, resultIntent: Intent) {
        val pendingIntent = PendingIntent.getActivity(ctx, 0, resultIntent, 0)
        val notification = Notification.Builder(ctx, CHANNEL_ID)
                .setContentText(text)
                .setSmallIcon(R.drawable.ic_launcher)
                .setChannelId(CHANNEL_ID)
                .setContentIntent(pendingIntent)
                .setGroup(NOTIFICATION_GROUP_KEY)
                .setGroupSummary(true)
                .build()

        notification.flags = Notification.FLAG_NO_CLEAR

        notificationManager.notify(id, notification)
        showSummaryNoti()
    }

    fun isPinnedItem(id: Int): Boolean {
        val notiList = notificationManager.activeNotifications

        for (n in notiList) {
            if (id == n.id) {
                return true
            }
        }

        return false
    }

    fun dismissNotification(id: Int) {

        notificationManager.cancel(id)
    }

    private fun showSummaryNoti() {
        val summaryNoti = NotificationCompat.Builder(ctx, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_launcher)
                .setStyle(NotificationCompat.InboxStyle()
                        .setSummaryText(ctx.getString(R.string.app_name)))
                .setGroup(NOTIFICATION_GROUP_KEY)
                .setGroupSummary(true)
                .build()

        notificationManager.notify(SUMMARY_NOTI_ID, summaryNoti)
    }
}

커스텀 달력 만들어보기

|

DayListBuilder.kt

import java.util.*
import kotlin.collections.ArrayList

data class DayInfo(
        val year: Int, val month: Int, val day: Int,
        val timestamp: Long, val inMonth: Boolean) {

    fun isToday(): Boolean {
        val c = Calendar.getInstance()
        val todayYear = c.get(Calendar.YEAR)
        val todayMonth = c.get(Calendar.MONTH)
        val todayDay = c.get(Calendar.DAY_OF_MONTH)

        return (todayYear == year) && (todayMonth == month) && (todayDay == day)
    }
}

class DayListBuilder {

    companion object {
        val instance = DayListBuilder()
    }

    private constructor()

    fun getDayList(year: Int, month: Int): ArrayList<DayInfo> {
        val list = ArrayList<DayInfo>()

        val c = Calendar.getInstance()

        c.set(Calendar.YEAR, year)
        c.set(Calendar.MONTH, month - 1)
        c.set(Calendar.DAY_OF_MONTH, 1)
        c.set(Calendar.HOUR_OF_DAY, 3)
        c.set(Calendar.MINUTE, 0)
        c.set(Calendar.SECOND, 0)

        // Add days of previous Month
        val prevMonthDays = c.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY
        c.add(Calendar.DAY_OF_YEAR, -prevMonthDays)
        for (day in 0 until prevMonthDays) {
            list.add(DayInfo(c.get(Calendar.YEAR), c.get(Calendar.MONTH),
                    c.get(Calendar.DAY_OF_MONTH), c.timeInMillis, false))
            c.add(Calendar.DAY_OF_YEAR, 1)
        }

        // Add days of target Month
        for (day in 1..c.getActualMaximum(Calendar.DAY_OF_MONTH)) {
            c.set(Calendar.DAY_OF_MONTH, day)
            list.add(DayInfo(c.get(Calendar.YEAR), c.get(Calendar.MONTH),
                    c.get(Calendar.DAY_OF_MONTH), c.timeInMillis, false))
        }

        // Add days of next Month
        val nextMonthDays = Calendar.SATURDAY - c.get(Calendar.DAY_OF_WEEK)
        for (day in 1..nextMonthDays) {
            c.add(Calendar.DAY_OF_YEAR, 1)
            list.add(DayInfo(c.get(Calendar.YEAR), c.get(Calendar.MONTH),
                    c.get(Calendar.DAY_OF_MONTH), c.timeInMillis, false))
        }

        return list
    }
}


CalendarAdapter.kt

import android.content.Context
import android.graphics.Color
import android.support.v7.app.AlertDialog
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.snowdeer.htracker.R
import com.snowdeer.htracker.model.OnDatabaseEventListener
import com.snowdeer.htracker.model.RewardModelManager
import com.snowdeer.htracker.model.data.HabitDto
import kotlinx.android.synthetic.main.item_calendar.view.*
import java.util.*
import kotlin.collections.ArrayList

class CalendarAdapter(
        private val ctx: Context, private val habit: HabitDto?
        , private var year: Int, private var month: Int
) : RecyclerView.Adapter<ViewHolder>(), OnDatabaseEventListener {

    private var list = ArrayList<DayInfo>()

    init {
        refresh()
        RewardModelManager.instance.setOnDatabaseEventListener(this)
    }

    private fun refresh() {
        list = DayListBuilder.instance.getDayList(year, month)
        notifyDataSetChanged()
    }

    fun refresh(year: Int, month: Int) {
        this.year = year
        this.month = month
        refresh()
    }

    override fun onDatabaseUpdated() {

    }

    fun getItem(position: Int): DayInfo {
        return list[position]
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
        val view: View = LayoutInflater.from(ctx).inflate(R.layout.item_calendar, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(holder.adapterPosition)

        holder.day.text = item.day.toString()
        holder.day.setTextColor(getDayColor(item))

        if (existStamp(item.year, item.month, item.day)) {
            holder.stamp.visibility = View.VISIBLE
        } else {
            holder.stamp.visibility = View.INVISIBLE
        }

        if (item.isToday()) {
            holder.background.setBackgroundColor(ctx.getColor(R.color.today_background))
        } else {
            holder.background.setBackgroundColor(ctx.getColor(R.color.transparent))
        }

        holder.background.setOnClickListener {
            if (habit != null) {
                if (item.isToday()) {
                    if (existStamp(item.year, item.month, item.day)) {
                        RewardModelManager.instance.delete(habit.id, item.year, item.month, item.day)
                    } else {
                        RewardModelManager.instance.create(habit.id, item.year, item.month, item.day)
                    }
                    notifyItemChanged(holder.adapterPosition)
                } else {
                    showForceHabitResultDialog(holder.adapterPosition, habit.id, item.year, item.month, item.day)
                }
            }
        }
    }

    private fun existStamp(year: Int, month: Int, day: Int): Boolean {
        if (habit == null) {
            return false
        }

        return RewardModelManager.instance.exist(habit.id, year, month, day)
    }

    private fun getDayColor(info: DayInfo): Int {
        val c = Calendar.getInstance()
        c.set(Calendar.YEAR, info.year)
        c.set(Calendar.MONTH, info.month)
        c.set(Calendar.DAY_OF_MONTH, info.day)

        if (info.inMonth) {
            when (c.get(Calendar.DAY_OF_WEEK)) {
                Calendar.SUNDAY -> return ctx.getColor(R.color.in_month_sunday)
                Calendar.SATURDAY -> return ctx.getColor(R.color.in_month_saturday)
            }
            return Color.BLACK
        } else {
            when (c.get(Calendar.DAY_OF_WEEK)) {
                Calendar.SUNDAY -> return ctx.getColor(R.color.out_month_sunday)
                Calendar.SATURDAY -> return ctx.getColor(R.color.out_month_saturday)
            }

            return Color.GRAY
        }
        return Color.BLACK
    }

    private fun showForceHabitResultDialog(pos: Int, habitId: Long, year: Int, month: Int, day: Int) {
        AlertDialog.Builder(ctx)
                .setTitle("It is not Today. Do you want to edit the result?")
                .setPositiveButton("Yes") { _, _ ->
                    if (existStamp(year, month, day)) {
                        RewardModelManager.instance.delete(habitId, year, month, day)
                    } else {
                        RewardModelManager.instance.create(habitId, year, month, day)
                    }
                    notifyItemChanged(pos)
                }
                .setNegativeButton("No") { _, _ ->
                }
                .show()
    }
}

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val background = view.item_layout!!
    val day = view.day!!
    val stamp = view.stamp!!
}


fragment_calendar.xml

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

  <LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
      android:id="@+id/month"
      android:textStyle="bold"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:layout_marginStart="8dp"
      android:layout_gravity="center_vertical"
      android:text="5"
      android:textColor="@color/black"
      android:textSize="34sp"/>

    <TextView
      android:id="@+id/year"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:layout_gravity="center_vertical"
      android:text="2019"
      android:textColor="@color/darkergray"
      android:textSize="16sp"/>
  </LinearLayout>

  <android.support.v7.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="8dp"/>

</LinearLayout>


CalendarFragment.kt

import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.GridLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.snowdeer.htracker.R
import com.snowdeer.htracker.model.data.HabitDto
import com.snowdeer.utils.Log
import kotlinx.android.synthetic.main.fragment_calendar.view.*
import java.util.*

private const val ARG_YEAR = "ARG_YEAR"
private const val ARG_MONTH = "ARG_MONTH"
private const val ARG_HABIT = "ARG_HABIT"

class CalendarFragment : Fragment() {

    private var year: Int = 2019
    private var month: Int = 1
    private var habit: HabitDto? = null

    private lateinit var adapter: CalendarAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            year = it.getInt(ARG_YEAR)
            month = it.getInt(ARG_MONTH)
            habit = it.getParcelable(ARG_HABIT)
        }

        Log.i("onCreate: $habit")
    }

    companion object {
        fun newInstanceWithMonthOffset(habit: HabitDto?, offset: Int): CalendarFragment {
            val c = Calendar.getInstance()
            c.add(Calendar.MONTH, offset)

            Log.i("newInstanceWithMonthOffset: $habit")
            return newInstance(habit, c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1)
        }

        private fun newInstance(habit: HabitDto?, year: Int, month: Int): CalendarFragment {
            return CalendarFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_YEAR, year)
                    putInt(ARG_MONTH, month)
                    putParcelable(ARG_HABIT, habit)
                }
            }
        }
    }

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_calendar, container, false)

        view.year.text = year.toString()
        view.month.text = month.toString()

        Log.i("onCreateView: $habit")
        adapter = CalendarAdapter(activity!!, habit, year, month)
        view.recycler_view.layoutManager = GridLayoutManager(activity, 7)
        view.recycler_view.adapter = adapter

        return view
    }
}

Realm 이용한 Database 사용 예제

|

프로젝트 build.gradle

buildscript {
    ext.kotlin_version = '1.3.31'
    repositories {
        google()
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "io.realm:realm-gradle-plugin:5.2.0"
    }
}


모듈 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'

...

dependencies {
    ...
    implementation 'io.realm:android-adapters:2.1.1'
}


SnowApplication.kt

import android.app.Application
import android.content.Context
import io.realm.Realm
import io.realm.RealmConfiguration

class SnowApplication : Application() {

    companion object {
        private var instance: SnowApplication? = null

        fun context(): Context {
            return instance!!.applicationContext
        }
    }

    init {
        instance = this
    }

    override fun onCreate() {
        super.onCreate()

        Realm.init(this)
        Realm.setDefaultConfiguration(getRealmConfig())
    }

    private fun getRealmConfig(): RealmConfiguration {
        return RealmConfiguration.Builder()
                .deleteRealmIfMigrationNeeded()
                .build()
    }
}


데이터 레코드 정의

open 키워드를 이용해서 상속을 가능하게 해야 하며, RealmObject()를 상속받아야 합니다.

import android.os.Parcel
import android.os.Parcelable
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey

open class RewardDto(
        var habitId: Long = 0,
        var year: Int = 0, var month: Int = 0, var day: Int = 0,
        var percent: Int = 0, var stampType: Int = 0) : RealmObject()

open class HabitDto(
        @PrimaryKey var id: Long = 0,
        var name: String = "", var seq: Long = 0) : RealmObject()


데이터 생성, 조회, 수정, 삭제

import com.snowdeer.htracker.model.data.HabitDto
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.createObject
import io.realm.kotlin.where

interface OnDatabaseEventListener {
    fun onDatabaseUpdated()
}

class HabitModelManager {

    companion object {
        val instance = HabitModelManager()
    }

    private constructor()

    private val realm = Realm.getDefaultInstance()

    private var onDatabaseEventListener: OnDatabaseEventListener? = null

    fun setOnDatabaseEventListener(l: OnDatabaseEventListener) {
        onDatabaseEventListener = l
    }

    fun notifyDatabaseUpdated() {
        onDatabaseEventListener?.onDatabaseUpdated()
    }

    private fun nextId(): Long {
        val maxId = realm.where<HabitDto>().max("id")
        if (maxId != null) {
            return maxId.toLong() + 1
        }
        return 0
    }

    private fun nextSeq(): Long {
        val maxSeq = realm.where<HabitDto>().max("seq")
        return maxSeq?.toLong()?.plus(1) ?: 0
    }


    fun getList(): ArrayList<HabitDto> {
        val realmResult = realm.where<HabitDto>()
                .findAll()
                .sort("seq", Sort.DESCENDING)

        val list = ArrayList<HabitDto>()
        for (item in realmResult) {
            list.add(item)
        }

        return list
    }

    fun create(name: String) {
        realm.beginTransaction()

        val item = realm.createObject<HabitDto>(nextId())

        item.name = name
        item.seq = nextSeq()

        realm.commitTransaction()

        notifyDatabaseUpdated()
    }

    fun update(id: Long, name: String, seq: Long) {
        realm.beginTransaction()

        val item = realm.where<HabitDto>().equalTo("id", id).findFirst()!!
        item.name = name
        item.seq = seq

        realm.commitTransaction()

        notifyDatabaseUpdated()
    }

    fun deleteWithReward(id: Long) {
        realm.beginTransaction()

        val item = realm.where<HabitDto>().equalTo("id", id).findFirst()!!
        item.deleteFromRealm()

        realm.commitTransaction()

        notifyDatabaseUpdated()
    }
}

File에 텍스트(Text) 읽고 쓰기

|

File에 텍스트 읽고 쓰는 예제

</pre class=”prettyprint”> import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter;

public class FileUtil {

public synchronized static void writeTextToFile(String filepath, String text) { File file = new File(filepath); FileWriter fw = null; BufferedWriter bw = null;

try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} catch (Exception e) {
  e.printStackTrace();
} finally {
  try {
    bw.close();
  } catch (Exception e) {
    e.printStackTrace();
  }

  try {
    fw.close();
  } catch (Exception e) {
    e.printStackTrace();
  }
}   }

public synchronized static String getTextFromFile(String filepath) { File file = new File(filepath);

StringBuilder sb = new StringBuilder();

try {
  BufferedReader br = new BufferedReader(new FileReader(file));
  String line;

  while ((line = br.readLine()) != null) {
    sb.append(line);
    sb.append('\n');
  }
  br.close();
} catch (Exception e) {
  e.printStackTrace();
}

return sb.toString();   } }

</pre>

CollapsingToolbarLayout 예제

|

styles.xml


<resources>

<style name=”AppTheme” parent=”Theme.AppCompat.Light.DarkActionBar”> <item name=”colorPrimary”>#80CBC4</item> <item name=”colorPrimaryDark”>#80CBC4</item> <item name=”colorAccent”>#3F51B5</item> </style>

<style name=”SnowDeerTheme” parent=”AppTheme”> <item name=”windowNoTitle”>true</item> <item name=”windowActionBar”>false</item> </style>

</resources>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
  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">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appBarLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

    <android.support.design.widget.CollapsingToolbarLayout
      android:id="@+id/collapsing_toolbar"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:fitsSystemWindows="true"
      app:contentScrim="@color/colorPrimary"
      app:expandedTitleMarginEnd="0dp"
      app:expandedTitleMarginStart="0dp"
      app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed">

      <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="12dp"
        android:orientation="vertical"
        app:layout_collapseMode="parallax"
        app:layout_collapseParallaxMultiplier="0.7">

        <ImageView
          android:layout_width="64dp"
          android:layout_height="64dp"
          android:layout_gravity="center_horizontal"
          app:srcCompat="@drawable/ic_launcher"/>

        <TextView
          style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_gravity="center_horizontal"
          android:text="@string/app_name"/>

      </LinearLayout>

      <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:popupTheme="@style/SnowDeerTheme"
        android:title="@string/app_name"
        app:layout_collapseMode="parallax"
        app:layout_scrollFlags="scroll|exitUntilCollapsed"/>

    </android.support.design.widget.CollapsingToolbarLayout>

    <RelativeLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

      <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:tabGravity="fill"
        app:tabMaxWidth="200dp"
        app:tabMode="scrollable"/>
    </RelativeLayout>
  </android.support.design.widget.AppBarLayout>

  <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="8dp"
    android:layout_marginBottom="8dp"
    android:layout_marginStart="8dp"
    android:layout_marginEnd="8dp"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <android.support.v4.view.ViewPager
      android:id="@+id/viewpager"
      android:layout_width="match_parent"
      android:layout_height="match_parent"/>

  </FrameLayout>

</android.support.design.widget.CoordinatorLayout>