76일차(1)/Android App(33) : SQLite(1)
** App에서 문자열을 영구 저장하는 방법
(영구 저장이란 앱을 종료하고 다시 시작해도 불러올 수 있는 문자열)
1. 파일 입출력을 이용해서 저장
2. android 내장 DataBase를 이용해서 저장
- SQLite DataBase (안드로이드에 기본으로 내장된 프로그램)
3. SharedPreference를 이용해서 저장 (느리지만 간단히 저장하고 불러올 수 있다)
- 내부적으로 xml 문서를 만들어서 문자열을 저장하고 불러온다.
- 저장된 문자열을 boolean, int, double, String type 으로 변환해서 불러올 수 있다.
- 3번 SharedPreference는 느리지만 간단히 저장해서 불러오기가 가능하다.
- xml 문서를 따로 저장해서 만들기 때문에, 파일을 불러오는 것만큼 느리다.
- xml파일을 FileReader를 사용해서 읽어오는 것이다.
- 그러면 어떤정보가 빠르고 정확하게 관리되어야 한다면?
→ 2번 SQLite DataBase 를 활용한다.
- 속도가 중요한 데이터를 관리해야 하는 경우, 빈번하게 수정이 일어나는 정보인 경우
ex)가계부
- 내장 데이터베이스 활용하기.
- SQL문을 사용한다.
- step14sqlite 모듈 생성
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">
<ListView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/listView"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="할일 입력..."
android:id="@+id/inputText"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="추가"
android:id="@+id/addBtn"/>
</LinearLayout>
</LinearLayout>
- 리니어 레이아웃으로 바꾸고 ListView, EditText 조합
- 할일을 입력하면 ListView 목록으로 출력되고, 그것이 실제로 DB에 저장되도록 해볼 예정!
- DB를 활용해서 입력/수정/삭제/목록보기 작업을 할 수 있다.
- 오라클을 설치해서 되는 것처럼! 안드로이드에는 미니 DB가 내장되어 있다.
- 자기만의 DB 파일을 만들어서 내장 어플리케이션을 사용해서 DB를 관리할 수 있다.
MainActivity.kt
package com.example.step14sqlite
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.*
import android.widget.AdapterView.OnItemLongClickListener
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() , View.OnClickListener {
//필요한 필드 정의하기
//lateinit 예약어를 사용하면 null을 넣을필 요없이 값을 나중에 넣을 수 있다.
//null을 대입했다가 나중에 값을 바꾸려면 번거롭다.
lateinit var inputText:EditText
lateinit var listView: ListView
lateinit var adapter: TodoAdapter
lateinit var helper: DBHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//필요한 UI의 참조값 얻어와서 필드에 저장하기
inputText=findViewById(R.id.inputText)
listView=findViewById(R.id.listView)
//버튼 리스너 등록하기
findViewById<Button>(R.id.addBtn).setOnClickListener(this)
//DBhelper 객체의 참조값을 필드에 저장하기
//version값이 증가되면 onUpgrade() 메소드가 자동 호출되면서 db가 초기화된다.
helper = DBHelper(this, "MyDB.sqlite", null, 2)
//할일 목록 얻어오기
var list:List<Todo> = TodoDao(helper).getList()
//listView 동작 준비하고, 할일 목록 출력하기
adapter=TodoAdapter(this, R.layout.listview_cell, list)
//listView에 아답타 연결하기
listView.adapter=adapter
}
override fun onClick(v: View?) {
//1. 입력한 문자열을 읽어와서
val msg=inputText.text.toString()
//2. Todo 객체에 담아서
val data=Todo(0, msg, "")
//3. TodoDao객체를 이용해서 DB에 저장한다.
TodoDao(helper).insert(data)
//4. 목록을 새로 얻어와서
val list=TodoDao(helper).getList()
//5. 아답타에 넣어주고
adapter.list=list
//6. 모델의 내용이 바뀌었다고 아답타에 알려서 ListView가 업데이트되도록 한다.
adapter.notifyDataSetChanged()
//7. Toast 띄우기
Toast.makeText(this, "저장했습니다.", Toast.LENGTH_SHORT).show()
inputText.setText("")
}
}
- inputText 필드를 정의(null이 들어갈수 있게)
- inputText의 참조값을 찾아와서 넣어줌
lateinit var inputText:EditText
- 번거로우면 이렇게! lateinit 예약어 사용
- 값을 나중에 넣는다는 것을 가정하고 그대로 두는 것. null을 넣거나 ? 표기를 하지 않아도 된다.
- 초기화를 나중에 할 것이다. 필드에 뒤늦은 초기화를 할때 사용한다.
inputText=findViewById(R.id.inputText)
listView=findViewById(R.id.listView)
- 이렇게 만들어둔 필드에 직접 값을 넣어준다.
- 뷰 바인딩을 쓸 때는 이렇게 쓸 필요 없다.
- 버튼리스너 등록! View.OnClickListener 를 구현하고 메소드 오버라이드
- 버튼 객체에 리스너 등록하기
- 입력받은 할일 하나하나를 객체로 관리한다.
- Todo 라는 data class 생성. java의 TodoDto 와 같은 것이라고 보면 된다.
Todo.kt
package com.example.step14sqlite
// data class는 java에서 TodoDto라고 생각하면 된다.
data class Todo(var num:Int, var content:String, var regdate:String)
- 이렇게 작성하면 setter, getter 가 자동으로 만들어진다.
Todoadapter 생성(listView에 연결할 것)
-> 이전에 만든 CountryAdapter (java 코드로 작성) 와 비교하며 보기
package com.example.step14sqlite
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
/*
Context type, Int type, list<Todo> type 을 인자로 전달받는 대표 생성자
인자로 전달받은 내용을 자동으로 필드에 저장한다.
BaseAdapter 추상 클래스를 상속받아서 만든다.
*/
class TodoAdapter(var context: Context, var layoutRes: Int, var list:List<Todo>) : BaseAdapter(){
//전체 sell의 갯수 리턴
override fun getCount(): Int {
return list.size
}
//인자로 전달된 position에 해당하는 아이템 리턴하기
override fun getItem(position: Int): Any {
return list.get(position)
}
//인자로 전달된 position 에 해당하는 아이템의 아이디 리턴하기
override fun getItemId(position: Int): Long {
return list.get(position).num.toLong() // Int type을 Long type으로 casting해서 리턴
}
//인자로 전달된 cell view를 리턴하는 메소드
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
//kotlin에서 메소드의 매개변수는 상수(val)이기 때문에 값 변경이 불가하다.
var resultView:View = if(convertView == null) LayoutInflater.from(context).inflate(layoutRes, parent, false)
else convertView
//position에 해당하는 할일 정보를 얻어와서
val data=list.get(position)
//TextView에 출력하고
val text_content=resultView.findViewById<TextView>(R.id.text_content)
val text_regdate=resultView.findViewById<TextView>(R.id.text_regdate)
text_content.text=data.content
text_regdate.text=data.regdate
//해당 View를 리턴해준다.
return resultView
}
}
- BaseAdapter를 상속하고, 메소드 4개 override
- 대표 생성자만 선언해주면 된다.
- 그러면 인자로 전달받은 내용이 자동으로 필드에 저장된다.
- this.xxx 라는 코드를 넣을 필요가 없다.
- 저 대표생성자 1줄로 CountryAdapter의 이 코드를 대체할 수 있다.
- 코틀린의 코드 단축, 효율성
- java였다면 들어가야 하는 코드! 이런 코드가 필요없다.
- Any (=object) 타입을 받으므로 position값으로 가져온 아이템을 리턴
- toLong() 으로 Long 타입으로 리턴
- java의 Long 타입과 같다.
- java에서는 좁은 범위의 타입을 넓은 범위의 타입에 담는것이 가능했다.
- 하지만 코틀린에서는 다르다! 위와 같이 int타입을 Long타입에 담으면 오류가 발생한다.
- 이런 경우 var num2:Long = num1.toLong() 형태로 넣어주어야 한다.
- CountryAdapter 에도 view를 리턴해주는 메소드가 있는데,
TodoAdapter에서도 position에 해당되는 뷰를 리턴
- java에서는 view를 받아서 view가 null이면 전개할 것을 받아서 리턴
메소드의 인자로 전달된 view 값을 조건부로 수정하기도 한다.(inflater로)
- 하지만 코틀린에서는? position에 다른값을 집어넣으려고 하면 오류가 발생한다! 다른 값을 넣을 수 없다.
- 지금 여기 들어가 있는 값은 상수화되어 있다(val). 따라서 인자로 전달된 값을 그대로 써야한다.
- 그래서 view타입 변수(resultView)를 만들어서 convertView를 넣어주는 방식으로 작성했다.
- inflater를 사용해서 레이아웃 전개하기
var resultView:View = if(convertView == null) LayoutInflater.from(context).inflate(layoutRes, parent, false)
else convertView
- 위 코드를 이런 코드로 수정해볼 수 있다.
- null 이면 초록색 박스의 리턴값을 대입하고,
null이 아니면 분홍색 화살표 convertView를 대입한다!
- 해당 값이 담긴 resultView가 최종적으로 리턴된다.
- java에서는 사용할 수 없는 방식. 문법이 유연하다.
- 코틀린에서도 if~else 를 활용해서 3항연산자와 비슷하게 사용할 수 있다.
- position이 값을 찾아오는 primary key 역할을 한다.
- cell 레이아웃 만들어주기
new- layout resource file
listview_cell.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">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/text_content"
android:textSize="20sp"
android:text="내용입니다..."
android:layout_margin="10dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/text_regdate"
android:textColor="#999999"
android:gravity="right"
android:text="등록일입니다..."
android:layout_margin="10dp"/>
</LinearLayout>
- 여기에서 부여한 id를 사용해 xml 문서를 전개해서 화면을 구성
- id를 사용해 참조값을 얻어와서 textView에 출력하기
- Custom Adapter에서 java 코드로 했던 작업이다.
- 코틀린으로 작성해본 것!
- 데이터에 list를 연결해줄 준비중 !
- 새 클래스 생성
DBHelper
package com.example.step14sqlite;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;
/*
DB 생성 및 리셋을 도와주는 도우미 클래스 만들기
- SQLiteOpenHelper 추상클래스를 상속받아서만든다.
*/
public class DBHelper extends SQLiteOpenHelper {
public DBHelper(@Nullable Context context, @Nullable String name,
@Nullable SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
//App에서 DB를 처음 사용할때 호출되는 메소드
@Override
public void onCreate(SQLiteDatabase db) {
//사용할 테이블을 만들면 된다.
//1. 실행할 sql문을 준비하고
String sql="CREATE TABLE todo "+
"(num INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, regdate TEXT)";
//2. SQLiteDataBase 객체를 이용해서 실행한다.
db.execSQL(sql);
}
//DB를 리셋(업그레이드)할때 호출되는 메소드
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//업그레이드할 내용을 작성하면 된다.
db.execSQL("DROP TABLE IF EXISTS todo"); //만일 테이블이 존재하면 삭제한다.
//다시 만들어질 수 있도록 onCreate() 메소드를 호출한다.
onCreate(db);
}
}
- DBHelper 클래스를 만드는 이유는? DB를 초기화하기 위해서
- 초기화하고 필요에 따라서 업그레이드를 하기위해서 이 클래스가 필요하다.
- SQLiteOpenHelper 클래스 상속
- SQLite : 데이터를 영구저장하고 관리할 수 있다. 안드로이드 기기 속 미니 DB라고 생각하면 된다!
- 메소드 2개(onCreate, onUpgrade) override하고 생성자도 가져와주기
- onCreate() : DB를 처음 호출할때 사용된다. sql문을 사용할 수 있다.
- execSQL() : onCreate 안에서 사용하는 sql문을 사용할 수 있는 메소드
- Primary Key 오라클과 동일하게 사용
- 데이터타입이 오라클과 다르다. INTEGER, TEXT => num, varchar2 라고 보면 된다.
- SQLite에는 날짜 타입이 없다. 대신 날짜 타입처럼 사용할 수 있는 문자열을 만들어서 사용한다.
- 오라클에서는 시퀀스를 만들어야 했지만,
SQLite에서는 AutoIncrement 하면 자동 증가되는 값이 알아서들어간다.
- 내부적으로 시퀀스를 만들어서 알아서 값을 넣어준다.
- DBHelper 클래스의 도움을받아서 DB 데이터 연결
- onUpgrade() : DB를 업그레이드한다. 업그레이드가 되면 onCreate가 다시 시작된다.
- table을 drop해버리고 다시 만들도록 코딩했다.
- onCreate() 는 일반적으로 최초에 한번만 호출된다.
- 호출할때 버전의 숫자가 바뀌지 않는 이상!
TodoDao
package com.example.step14sqlite
import android.database.Cursor
/*
TodoDao 클래스의 대표생성자(primary constructor) 의 인자로 DBHelper 객체를 전달받아서
필드에 넣어두고 메소드에서 활용하는 구조이다.
insert, update, delete 작업을 할때는
val db:SQLiteDataBase = helper.getWritableDataBase()
val db:SQLiteDataBase = helper.writableDataBase()
select 작업을 할때는
val db:SQLiteDataBase = helper.readableDataBase()
java의 JDBC에서 PreparedStatement 객체와 비슷한 기능을 하는 객체가 SQLiterDataBase 객체이다.
*/
class TodoDao(var helper:DBHelper) {
//할일 정보를 저장하는 메소드
fun insert(data:Todo){
//java => SQLiteDataBase db=helper.getWritableDataBase()
val db=helper.writableDatabase
// ? 에 바인딩할 인자를 Any 배열로 얻어내기
//java => Object[] args = { data.getContent() }
var args= arrayOf<Any>(data.content)
//실행할 sql문
// SQLiteDB => datetime('now', 'localtime'), oracle => SYSDATE
val sql = "INSERT INTO todo (content, regdate)" +
" VALUES(?, datetime('now', 'localtime'))"
//sql문 실행하기
db.execSQL(sql, args)
db.close() //close() 를 호출해야 실제로 반영된다.
}
//할일 목록을 리턴하는 함수
fun getList():List<Todo>{
//수정가능한 todo type을 담을 수 있는 리스트
val list= mutableListOf<Todo>()
val db=helper.readableDatabase
//실행할 select 문 구성 (binding 할것은 없음)
val sql="SELECT num, content, regdate FROM todo ORDER BY num ASC"
//query 문을 수행하고 결과를 Cursor 객체로부터 얻어내기 (selection 인자는 없다.)
val result:Cursor = db.rawQuery(sql, null)
//Cursor 객체의 메소드를 이용해서 담긴 내용을 반복문 돌면서 호출한다.
while (result.moveToNext()){
//0번째는 num, 1번째는 content, 2번째는 regdate이다.
val data=Todo(result.getInt(0), result.getString(1), result.getString(2))
//할일 정보가 담긴 Todo 객체를 List에 추가한다.
list.add(data)
}
//할일 목록을 리턴해주기
return list
}
}
class TodoDao(var helper:DBHelper) { }
- 대표 생성자의 인자로 DBHelper를 전달받게 한다.
- todo를 전달받아서 DB에 저장한다.
- getter 메소드를 이렇게 필드를 참조하는 모양으로 사용한다.
- DBHelper 객체를 통해서 SQL 데이터베이스 객체를 얻어낸다!
- JDBC에서 사용하던 PreparedStatement와 비슷한 역할을 한다.(순수 jsp에서 사용)
- 이 DB 객체를 사용해서 sql문을 수행한다.
- execute SQL 메소드를 사용해서 실행할 sql 문자열과 저장할 Any 타입을 가지고 오는 것이다.
arrayOf<Any>()
- 바인딩할 내용을 배열Array에 담아서 전달해준다.(Any 타입을 담을 수 있는 array)
- Todo 할일(content) 의 내용이 이곳에 바인딩되어 들어간다.
- 만약 들어갈 ? 의 내용이 여러개라면 이 배열에 여러가지 정보를 담아서 각각의 ?에 바인딩해주면 된다
- 배열 args를 인자로 전달하기만 하면 순서대로 자동으로 바인딩해준다.
- java의 object 배열이 코틀린에서는 Array<Any> 라고 생각하면 된다.
- SQLite에는 날짜유형은 없지만 datetime() 이라는 함수를 통해서 현재 날짜를 집어넣어주면 된다.
- now, localtime을 인자로 넣어주면 된다.(세계시간 기준이 아니라 지역시간 기준이라는 의미로)
- 그러면 oracle의 sysdate와 같은 기능으로 사용할 수 있다.
- 할일 목록을 리턴해주려면 반복문을 돌면서 list에 할일 목록을 담아야 하기 때문에 mutableList<>, .add() 를 사용했다.
- 수정 가능한 todo타입을 담을 수 있는 리스트!
- rawQuery는 Cursor타입을 리턴한다.
- Cursor 타입은 jsp에서 사용하던 resultSet 역할이라고 보면 된다. select의 결과를 담아준다.
- Result는 Cursor 객체 타입이다. 타입 추론이 가능해서 생략한 것!
- 쿼리문을 수행해서 Cursor객체를 리턴해서 .moveToNext() 메소드로 커서가 위치한 곳의 결과를 얻어내는 것.
- 돌면서 출력해준다. 데이터를 빼낼때는 타입에 맞춰 getXX 메소드를 사용하면 된다.
- 칼럼명으로 출력하는 기능은 없다. 칼럼의 번호로 가지고 와야 한다.
- 일반적으로 getInt, getString 을 가장 많이 사용한다.
- getBlob() : 2진데이터를 불러낼때사용
- result의 0번째, 1번쨰, 2번째가 이것에 해당한다.
- 가져올 타입에 맞는 getter 메소드를 사용해주면 된다.
- 추가 버튼 클릭시 해야할 동작
- todo에는 지금 content만 담겨있다.
- Todo() 를 사용해서 담고 싶은데 현재 Todo에는 이런 디폴트 생성자가 없다.
- null은 넣을 수 없으므로, "" 이런식으로 넣어줄 수도 있다.
- content만 전달하고 나머지 num과 regdate에는 0과 빈 문자열을 전달하면 된다!
- onClick 메소드에서도 접근할 수 있게 하기위해 DBHelper를 나중에 값을 담을 필드로 만들어준다.
- 4개의 인자를 넣어서 DBHelper를 호출한다.
- 만약 DB내용을 갈아엎고 싶다면 마지막 인자 version에 2를 넣어주면 된다.
- DB내용이 초기화되면서 내용을 바꾸게 할 수 있다.
- 이 MainActivity는 자주 활성화된다. onCreate()가 호출될 일이 많다.
- 하지만 생성할때마다 DB를 새로 세팅하지는 않는다.(version 번호에 따라 새로 생성할지 여부가 달라진다)
- 입력한 할일이 이렇게 listView의 목록에 담긴다.
- 이 목록은 앱을 종료시켰다가 다시 실행해도 살아있다.
- 이 데이터는 일부러 지우지 않는 이상 영구 저장이다
- Device file explorer에서 보면 앱이 사용하는 고유한 sandbox가 있다. 이것은 앱의 패키지명으로 만들어진다.
- 여기가 앱의 내부(internal) 저장공간이다.
- 이 내부 저장공간 안에 databases 폴더가 만들어져 있다.
databases 폴더 안에 DBHelper에서 문자열로 전달했던 이름("MyDB.sqlite")으로 파일이 만들어져있다.
- 원한다면 이것을빼서 SQLite로 열 수 있는 프로그램으로 열어서 확인해 볼 수도 있다.
- 그러면 저 안에 저장된 테이블, 저장된 데이터 등등을 볼 수 있다.
- DB파일 자체는 앱에 저장되고,
저 파일을 사용할 수 있는 기능은 안드로이드 기기에서 내장되어 가지고 있다.
- MainActiviy에서 인자로 전달하는 version의 숫자를 바꾸면?
- version up 하면 내용이 갈아 엎어진다. table의 내용이 깨끗하게 사라져 있다.
- 버전이 달라지면 onUpgrade() 가 호출되면서 테이블이 드랍된다.
- Drop table 하고 onCreate에서 테이블을 다시 만든 것이다.
- 지금 배우고 있는 것은 안드로이드 Native 앱 개발이다.
- 안드로이드의 고유한 ui를 사용 (버튼, textview, ...)
- 이 바탕으로 IOS Native App 개발은 할 수 없다.
- IOS만의 앱개발 UI가 따로 있기때문에. IOS 앱개발을 위해서는 swift를 배워야 한다.
- native를 바탕으로 만드는 것이 깔끔하고 빠르지만, 대부분의 앱들이 그렇게 빠른 속도를 요구하지는 않는다.
(게임 등 특수한 경우가 아니라면)
- 웹브라우저의 기능을 앱으로 보여주는 정도라면 그렇게 엄청나게 빠르지는 않아도 된다.
- 고성능 앱을 요구하는 경우가 별로 없으므로..
→ 그러면 그런 앱을 꼭 안드로이드/IOS로 구분해야 할까? 하나를 만들어서 같이 사용할 방법은?
- 이렇게 안드로이드, IOS 겸용으로 앱개발을 하려면? 하이브리드 앱을 개발하면 된다.
- 네이티브 앱에 비해 성능은 조금 떨어지지만 비용절감 때문에...
- html, css, javascript 를 활용해서 만드는 것이 Hybrid app이다.
- 원래 웹브라우저가 해석해주는 것인데? 앱으로도 활용할 수 있다.
- html, css, javascript 가 해석할 수 있는 웹뷰를 만들어서 앱 위에 펼친다.
- 이런 형태로 앱을 개발하는 것이 hybrid app이다.
- 화면만 똑같이 나온다고 될까? 앱의 하드웨어를 사용해야 할 수도 있는데?
(연락처나 카메라 사용, 위치정보 접근, 갤러리 접근 및 이미지 사용, 흔들림/기울기 등 기기의 센서값, ...)
→ hybrid앱을 만들려면 하드웨어에 접근할 수 있는 코딩을 미리 해놓아야 한다.
하드웨어에 접근하기 위한 코딩은 각각의 native 언어로 프로그래밍할 수밖에 없다.
- 하이브리드 앱을 개발할 수 있는 프레임워크가 있다.
여기서 이런 코딩을 미리 해놓았다.(코틀린,swift로)
- 미리 만들어놓은 기능을 javascript로 call해서 사용할 수 있도록 만든다.
- 이런 프레임워크중 제일 많이 쓰이는것은 1) React Native, 2) Flutter 이다.
- 안드로이드 하드웨어, IOS 하드웨어를 사용할 코딩이 필요하면 이 프로그램 안에 미리 되어있는 코딩을 가져다 쓸 수 있다.
- Android 앱개발에 관심이있다면 java/kotlin 을 잘 배워두고 하이브리드 쪽으로 넘어가면 좋다.
- flutter는 언어가 다르다. dart 라는 언어를 사용한다.
- 이런 프로그램들을 익혀두면 안드로이드, ios앱을 비교적 쉽게 만들 수 있다.
'국비교육(22-23)' 카테고리의 다른 글
76일차(3)/Android App(35) : WebView (1) | 2023.01.26 |
---|---|
76일차(2)/Android App(34) : SQLite 활용(2) (0) | 2023.01.26 |
75일차(3)/Android App(32) : Shared Preferences(2) / menu (1) | 2023.01.26 |
75일차(2)/Android App(31) : Shared Preferences(1) (0) | 2023.01.25 |
75일차(1)/Android App(30) : Kotlin loop, try-catch (0) | 2023.01.25 |