728x90

Material Design Navigation Drawer


Material Design 에 대한 자료는 https://github.com/material-components/material-components-android 를 참조하면 도움된다.

안드로이드 네비게이션은 왼쪽에 슬라이드 형식으로 메뉴가 나왔다 들어갔다 하는 패널을 말한다.


앱 build.gradle 추가 사항

implementation 'com.google.android.material:material:1.1.0'


먼저 Material Design 사이트에서 제공하는 예제 파일을 열어봤더니 아래와 같은 예제가 나온다.

자식 View는 반드시 2개만 가진다는 점을 기억하자.

순서는 content 자식 View가 먼저 나오고 drawer View는 나중에 나온다.

그러면 drawer View가 나중에 나오기만 하면 되는 것인가? 아니다.


<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <!-- As the main content view, the view below consumes the entire
         space available using match_parent in both dimensions. Note that
         this child does not specify android:layout_gravity attribute. -->
    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- android:layout_gravity="start" tells DrawerLayout to treat
         this as a sliding drawer on the starting side, which is
         left for left-to-right locales. The navigation view extends
         the full height of the container. A
         solid background is used for contrast with the content view.
         android:fitsSystemWindows="true" tells the system to have
         DrawerLayout span the full height of the screen, including the
         system status bar on Lollipop+ versions of the plaform. -->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#333"
        android:fitsSystemWindows="true"
        app:menu="@menu/navigation_view_content"
        app:itemIconTint="@color/emerald_translucent"
        app:itemTextColor="@color/emerald_text"
        app:itemBackground="@color/sand_default"
        app:itemTextAppearance="@style/TextMediumStyle" />

</androidx.drawerlayout.widget.DrawerLayout>


위와 같이 하거나 또는

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <!-- 1. 콘텐츠 영역-->
    <include
        layout="@layout/content_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- 2. 왼쪽 사이드 메뉴-->
    <include layout="@layout/nav_header_main" />   

</androidx.drawerlayout.widget.DrawerLayout>


아래 nav_header_main.xml 파일을 보면 com.google.android.material.navigation.NavigationView 대신에 LinearLayout 으로 되어 있다. 꼭 com.google.android.material.navigation.NavigationView 를 사용하지 않아도 된다는 의미다.

android:layout_gravity="start" 를 추가해서 왼쪽 슬라이드 메뉴로 사용하겠다는 것만 포함되어 있다.


<?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:id="@+id/navi_view"
    android:layout_width="280dp"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:background="@color/colorWhite"
    android:orientation="vertical"
    android:theme="@style/ThemeOverlay.AppCompat.Dark"
    >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ededed"
        android:orientation="vertical"
        android:padding="10dp">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/img_info" />

            <ImageView
                android:id="@+id/btn_close"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="right|top"
                android:onClick="NaviItemSelected"
                android:src="@drawable/ic_close" />

        </FrameLayout>
    </LinearLayout>

</LinearLayout>
 


이제 자바 소스를 구현하는 걸 살펴보자.

public class MainActivity extends AppCompatActivity {
    Context context;
    DrawerLayout drawer_layout;
    ActionBarDrawerToggle toggle;
    //NavigationView navigationView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = MainActivity.this;

        drawer_layout = findViewById(R.id.drawer_layout);
        toggle = new ActionBarDrawerToggle(
                this, drawer_layout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawer_layout.addDrawerListener(toggle);
        toggle.syncState();

        // com.google.android.material.navigation.NavigationView 를 구현했을 때 처리 (구현 안해도 됨)
        //navigationView = findViewById(R.id.nav_view);
        //navigationView.setItemIconTintList(null);
        //navigationView.setNavigationItemSelectedListener(this);

        // content_main.xml 에서 지정한 버튼을 클릭하면 drawer 가 오픈되도록 함.
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               drawer_layout.openDrawer(GravityCompat.START);
            }
        });

    }

    // drawer 오픈된 상태에서 실행할 때 동작 이벤트 처리
    public void NaviItemSelected(View view) {
        switch (view.getId()) {
            case R.id.btn_close:
                drawer_layout.closeDrawer(Gravity.LEFT, true);
                break;
            case R.id.btn_logout:
                backPressHandler.appShutdown();
                break;
        }
    }
}


구현을 위한 핵심사항만 살펴본 것이므로 실제 구현 예제는 다른 블로그를 찾아보면 도움될 것이다.

예전 예제들은 android.support.v4.widget.DrawerLayout 로 되어 있는데 androidx.drawerlayout.widget.DrawerLayout 로 변경되었다는 것만 알자.


참고하면 좋은 자료

https://coding-factory.tistory.com/207 네비게이션 Drawer 사용법

https://medium.com/quick-code/android-navigation-drawer-e80f7fc2594f



블로그 이미지

Link2Me

,
728x90

탭을 누를 때마다 아래 화면이 마치 페이지가 넘어가듯이 변하는 것을 ViewPager 를 이용해 구현이 가능하다.

예전에 사용하던 방식에서 계속 새로운 기능으로 개선이 되면서 ViewPager를 이용한 TabLayout 을 연동하는 것이 가장 좋은 방법이다.

material Design에서 제공하는 tabLayout 을 이용하는 방법으로 된 예제다.

https://www.youtube.com/watch?v=YWjyAfEacT8 유투브 동영상 강의 보고 테스트한 걸 적어두었다. (2020.3.27)

그런데 제대로 된 코드가 아니라서 새롭게 수정 구현하고 적어둔다.(Upated 2020.4.26)


앱 gradle 에 라이브러리 추가하기

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.1.0'
}


TabLayout 사용하기

검색창에서 tab 을 입력하면 tabLayout 이 나온다. 마우스로 끌어다가 1번과 같이 놓는다.

material Design 에서 제공하는 tabLayout 이 추가된다.

하는 방법은 https://www.youtube.com/watch?v=YWjyAfEacT8 을 참조하면 된다.

ConstraintLayout 작성하는 방법에 대한 설명이라고 보면 된다.


https://stackoverflow.com/questions/31640563/how-do-i-change-a-tab-background-color-when-using-tablayout

에서 선택된 아이템 배경색을 지정하는 걸 참조해서 구현한다.


android:layout_marginTop="40dp" 으로 해줘야만 공간이 제대로 나오는 버그(?)가 있어서

LinearLayout 으로 변경해서 해결했다.


<?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">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        app:tabTextColor="@color/colorDarkGray"
        app:tabSelectedTextColor="@color/colorWhite"
        app:layout_collapseMode="pin"
        app:tabGravity="fill"
        app:tabBackground="@drawable/tab_color_selector"
        app:layout_constraintBottom_toTopOf="@+id/vp_layout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Monday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Tuesday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Wednesday" />
    </com.google.android.material.tabs.TabLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/vp_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tab_layout">

        <androidx.viewpager.widget.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

최근에는 ViewPager2 를 사용하라고 나오는 거 같은데 이건 나중에 테스트해볼 예정이다.


ViewPager 영역에 표시될 Fragment Layout 파일을 추가한다. 3개, 5개는 정하기 나름이다.

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
 

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


Fragment1.java 파일

- Activity 에서 전달받은 데이터를 Fragment 에서 받아서 처리하는 코드를 추가했다.

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class Fragment1 extends Fragment {
    private View view;

    private String parameter;


    public static Fragment1 newInstance(String parameter){
        Fragment1 fragment1 = new Fragment1();
        Bundle args = new Bundle(); //Use bundle to pass data
        args.putString("parameter", parameter);
        instance.setArguments(args); //Finally set argument bundle to fragment
        return instance;
    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            parameter = getArguments().getString("parameter");
            Log.e("FragParameter", parameter);
        }
    }


    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_1,container,false);
        return view;
    }


    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }
}

public class Fragment2 extends Fragment {
    private View view;

    public static Fragment2 newInstance(){
        Fragment2 fragment2 = new Fragment2();
        return fragment2;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_2,container,false);
        return view;
    }
}
 

public class Fragment3 extends Fragment {
    private View view;

    public static Fragment3 newInstance(){
        Fragment3 fragment3 = new Fragment3();
        return fragment3;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_3,container,false);
        return view;
    }
}
 



ViewPagerAdapter.java

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;

import java.util.ArrayList;
import java.util.List;

class ViewPagerAdapter extends FragmentPagerAdapter {
    private final List<Fragment> mFragmentList = new ArrayList<>();
    private final List<String> mFragTitleList = new ArrayList<>();

    public ViewPagerAdapter(FragmentManager manager) {
        super(manager);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public void addFrag(Fragment fragment, String title) {
        mFragmentList.add(fragment);
        mFragTitleList.add(title);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragTitleList.get(position);
    }
}


MainActivity.java

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;

import android.os.Bundle;

import com.google.android.material.tabs.TabLayout;

public class MainActivity extends AppCompatActivity {
    private ViewPager viewPager;
    private ViewPagerAdapter adapter;
    private TabLayout tabLayout;


    String code;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        code = getIntent().getExtras().getString("code");
        Log.e("MainActivityCode",code);


        Fragment frag1 = new Fragment1().newInstance(code);

        viewPager = findViewById(R.id.viewPager);
        tabLayout = findViewById(R.id.tab_layout);
        adapter = new ViewPagerAdapter(getSupportFragmentManager());
        adapter.addFrag(frag1, "Web");
        adapter.addFrag(new Fragment2(), "사랑");
        adapter.addFrag(new Fragment3(), "우정");
        viewPager.setAdapter(adapter);
        tabLayout.setupWithViewPager(viewPager);
    }
}


여기까지 하면 잘 동작할 것이다.


참고하면 좋을 자료

출처 : https://guides.codepath.com/android/viewpager-with-fragmentpageradapter

public class FirstFragment extends Fragment {
    // Store instance variables
    private String title;
    private int page;

    // newInstance constructor for creating fragment with arguments
    public static FirstFragment newInstance(int page, String title) {
        FirstFragment fragmentFirst = new FirstFragment();
        Bundle args = new Bundle();
        args.putInt("someInt", page);
        args.putString("someTitle", title);
        fragmentFirst.setArguments(args);
        return fragmentFirst;
    }

    // Store instance variables based on arguments passed
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        page = getArguments().getInt("someInt", 0);
        title = getArguments().getString("someTitle");
    }

    // Inflate the view for the fragment based on layout XML
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_first, container, false);
        TextView tvLabel = (TextView) view.findViewById(R.id.tvLabel);
        tvLabel.setText(page + " -- " + title);
        return view;
    }
}
 


블로그 이미지

Link2Me

,
728x90

Java 로 코딩된 프로젝트에 코틀린 코드 일부를 추가하는 걸 테스트 해보고 간단하게 적어둔다.


프로젝트 build.gradle

buildscript {
    ext.kotlin_version = '1.3.71'
    repositories {
        google()
        jcenter()
       
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}


앱 build.gradle

apply plugin: 'com.android.application'
// 코틀린 혼용 사용을 위해 추가
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.javakotlin"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.annotation:annotation:1.1.0'

    implementation 'com.google.android.material:material:1.0.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'

    implementation 'com.squareup.retrofit2:retrofit:2.7.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
}


Java 코드 파일과 kotlin 코드 파일을 혼용하여 사용해봤는데 잘 동작하는 걸 확인할 수 있었다.

코틀린을 완벽하게 이해한 상태이면 Java 코드 필요없겠지만 코틀린 문법 공부 하다 말아서 이렇게라도 해보는 것이 의의가 있다.


블로그 이미지

Link2Me

,
728x90

Meterial Design 로그인 예제 Layout 을 적어둔다.


<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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">

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

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingBottom="@dimen/activity_vertical_margin">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="300dp"
                android:gravity="center"
                android:text="Login"
                android:textStyle="bold"
                android:textSize="45dp"
                android:fontFamily="cursive"
                android:textColor="@color/colorWhite"
                android:layout_marginTop="0dp"
                android:background="@drawable/logo"/>

            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:layout_marginRight="20dp">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_id"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@color/colorWhite"
                    android:autofillHints="@string/prompt_id"
                    android:hint="아이디"
                    android:selectAllOnFocus="true" />

            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="10dp"
                android:layout_marginRight="20dp">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_pw"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:autofillHints="@string/prompt_pw"
                    android:hint="비밀번호"
                    android:background="@color/colorWhite"
                    android:imeOptions="actionDone"
                    android:inputType="textPassword"
                    android:selectAllOnFocus="true" />

            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.button.MaterialButton
                android:id="@+id/btn_login"
                style="@style/Widget.AppCompat.Button.Borderless.Colored"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="50dp"
                android:layout_marginRight="20dp"
                android:padding="10dp"
                android:text="로그인"
                android:textColor="@color/colorWhite"
                android:textSize="16sp"
                android:textStyle="bold"
                app:backgroundTint="@color/colorMint"
                app:cornerRadius="20dp" />

        </LinearLayout>
    </FrameLayout>

</ScrollView>


이미지 파일

logo.zip



'안드로이드 > Layout' 카테고리의 다른 글

Floating Action Button  (0) 2020.07.11
CardView Layout 예제  (0) 2020.04.12
ScrollView  (0) 2019.12.20
Dynamic Layouts  (0) 2019.01.06
Android Fragment 기본 예제  (0) 2018.09.11
블로그 이미지

Link2Me

,
728x90

그동안은 Retrofit을 사용할 일이 전혀 없었는데, 사용할 일이 있어서 관련 자료를 찾아서 테스트하고 적어둔다.

예제를 이해하는데 참 어려웠다. 그래서 유투브 동영상도 보고 이해하고 따라해보고 동작되는 걸 확인한 후에야 로직이 이해가 되었다.

구글에서 추천하는 Volley 라이브러리보다 속도가 빠르다고 검색결과가 나온다.

 

Retrofit 은 안전한 타입 방식의 HTTP 클라이언트로서 Android 와 Java 애플리케이션을 위한 라이브러리이다.

Json converter : JSON 타입의 응답결과를 객체로 매핑(변환)해주는 Converter

https://square.github.io/retrofit/ 를 참조한다. 그런데 설명만으로는 이해하기 어렵더라.

 

이해를 돕기 위해서 그림으로 설명한다.

Interface 생성

이 부분에는 GET/POST/PUT/DELETE 등 필요한 모든 함수를 선언하는 부분이다.

Call <> 사이에는 서버의 Json 형태에 따라 클래스 파일을 만들어서 넣어주면 된다.

/**
 * GET 방식, URL/getphoto.php/{code} 호출.
 * Data Type의 여러 개의 JSON을 통신을 통해 받음.
 * 주소값이 "http://www.abc.com/getphoto.php?code=1" 이 됨.
 * @param code 요청에 필요한 code
 * @return 다수의 Data 객체를 JSON 형태로 반환.
 */
@GET("/getphoto.php")
Call<Data> getData(@Query("code") String code);

 

 

앱 build.gradle 설정

    compileOptions { // 이거 안해주면 에러가 나오더라.
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

dependencies {
    implementation 'com.google.android.material:material:1.0.0'
    implementation 'com.squareup.retrofit2:retrofit:2.7.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
}

 

AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />

android:usesCleartextTraffic="true"

 

activity_login.xml

구글에서 Meterial Design 예제를 검색하면 나온다. 그걸 참조해서 하면 된다.

 

Resource 관련 파일을 첨부한다.

res.zip
다운로드

 

 

LoginActivity.java

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class LoginActivity extends AppCompatActivity implements View.OnClickListener {
    Context context;
    EditText etId;
    EditText etPw;
    String userID;
    String userPW;
    String uID; // 스마트 기기의 대체 고유값

    LoginAPI mloginService;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(LoginActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        context = LoginActivity.this;
        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{
                            android.Manifest.permission.READ_PHONE_STATE,
                            android.Manifest.permission.READ_CALL_LOG, // 안드로이드 9.0 에서는 이것도 추가하라고 되어 있음.
                            android.Manifest.permission.CALL_PHONE,  // 전화걸기 및 관리
                            android.Manifest.permission.ACCESS_FINE_LOCATION
                    })
                    .check();

        } else {
            initView();
        }
    }

    private void initView() {
        etId = findViewById(R.id.et_id);
        etPw = findViewById(R.id.et_pw);
        Button btn_login = findViewById(R.id.btn_login);

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Value.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .client(createOkHttpClient())
                .build();


        mloginService = retrofit.create(LoginAPI.class);
    }

    @Override
    public void onClick(View v) {
        userID = etId.getText().toString().trim();
        userPW = etPw.getText().toString().trim();
        uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
        mloginService.Login(userID, userPW, uID)
                .enqueue(new Callback<LoginResult>() {
            @Override
            public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
                // 네트워크 통신 성공
                LoginResult result = response.body();
                if(result.getResult().contains("success")){
                    Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                    intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY);
                    startActivity(intent);
                    finish();
                } else {
                    showAlert(result.getResult(),result.getMessage());
                }
            }

            @Override
            public void onFailure(Call<LoginResult> call, Throwable t) {
                // 네트워크 통신 실패
            }
        });
    }



    private OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(interceptor);
        return builder.build();
    }

    public void showAlert(String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(title);
        builder.setMessage(message)
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }
}

public interface LoginAPI { // 서버에 호출할 메소드를 선언하는 인터페이스
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(Value.URL_LOGIN)
    Call<LoginResult> Login(
            @Field("userId") String userId,
            @Field("userPw") String userPw,
            @Field("uuid") String uuid
            );
}

public class LoginResult { // 결과를 받을 모델
    private String result;
    private String message;

    public String getResult() {
        return result;
    }

    public String getMessage() {
        return message;
    }
}

public class Value extends AppCompatActivity {
    public static final String BASE_URL = "http://www.abc.com/mobile/";
    public static final String URL_LOGIN = "login.php";
}

 

예제는 결과를 받아서 RecyclerView 에 보여준다거나 후속 action 에 대한 건 기술하지 않았다.

이 예제는 그냥 복사해서 붙여넣기를 어떻게 하는지에 대한 설명이라고 보면 된다.

PHP 파일에 대한 사항은 기술하지 않는다. 이미 본 블로그 다른 예제를 보면 충분히 이해할 수 있으리라.

 

참고하면 도움되는 글

https://thdev.tech/androiddev/2016/11/13/Android-Retrofit-Intro/

 

https://link2me.tistory.com/1850 ArrayList 처리 예제

 

 

Update : 2020.8.3일

ㅇ Retrofit2 라이브러리를 여러번 호출하는 걸 만들어야 한다면 함수화를 하는 게 좋다.

 

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitAPI {
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        retrofit = new Retrofit.Builder()
                .baseUrl(Value.IPADDRESS)
                .addConverterFactory(GsonConverterFactory.create())
                .client(createOkHttpClient())
                .build();

        return retrofit;
    }

    private static OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(interceptor);
        return builder.build();
    }
}

 

사용 예제

public interface RemoteService {// 서버에 호출할 메소드를 선언하는 인터페이스
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(RetrofitUrl.URL_Verson)
    Call<VersionResult> lastVersion(
            @Field("os") String os
    );

    @FormUrlEncoded
    @POST(RetrofitUrl.URL_GroupList)
    Call<GroupResult> getGroupList(
            @Field("keyword") String keyword
    );
}

class RetrofitUrl {
    companion object {
        const val URL_Verson = "lastVersion.php"
        const val URL_GroupList = "groupList.php"
    }
}

private void UpgradeChk() {
    mloginService = RetrofitAPI.getClient().create(RemoteService.class);
    mloginService.lastVersion("0").enqueue(new Callback<VersionResult>() {
        @Override
        public void onResponse(Call<VersionResult> call, Response<VersionResult> response) {
            VersionResult result = response.body();

            version = Value.VERSION; // 앱 버전
            version = version.replaceAll("[^0-9]", ""); // 버전에서 숫자만 추출
            Log.d("WEB", "Response: " + response);

            String Response = result.getVersion().replaceAll("[^0-9]", ""); // 버전에서 숫자만 추출
            System.out.println("Server Version : " + Response);

            if (Integer.parseInt(version) < Integer.parseInt(Response)) { // 서버 버전이 더 높으면
                UpgradeProcess();
                if(Build.VERSION.SDK_INT >= 26)  {
                    NonSecretApp_Setting(); // 출처를 알 수 없는 앱 설정 화면 띄우기
                } else {
                    startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
                }
            } else {
                AutoLoginProgress();
            }
        }

        @Override
        public void onFailure(Call<VersionResult> call, Throwable t) {

        }
    });
}

 

더 세부적인 것은 Github 에 올려진 소스코드를 참조하면 도움될 것이다.

https://github.com/jsk005/JavaProjects/tree/master/retrofitsample

 

GitHub - jsk005/JavaProjects: 자바 기능 테스트

자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

블로그 이미지

Link2Me

,
728x90
안드로이드 10 에서는 TelephonyManager에서 개인을 특정할 수 있는 정보를 가져올수 없도록 변경되었다.

TelephonyManager().getDeviceId()
TelephonyManager().getImei()
TelephonyManager().getMeid()

그리고 하드웨어의 시리얼넘버도 사용이 불가능해졌다.
Builde.SERIAL


In android 10, couldn't get device id using permission "READ_PHONE_STATE".


getDeviceId() has been deprecated since API level 26.
You can use an instance id from firebase e.g FirebaseInstanceId.getInstance().getId();.
String deviceId = android.provider.Settings.Secure.getString(
                context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);


삼성갤럭시 S10(Android 10) 에서 테스트한 결과 코드를 적어둔다.

기존 안드로이드 폰은 IMEI 값이 15자리 숫자로만 되어 있었는데

아래 코드에서 제공하는 deviceID값은 IMEI 값이 아닌 다른 값을 제공하는 거 같다.

개발자모드에서 수집한 deviceID 와 release 모드로 앱을 만든 것과 값이 다르더라.

cc64a5b80853bc8d

36f7660af1dc22f2


public static String getDeviceId(Context context) {

    // 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
    String deviceId;

    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
    } else {
        final TelephonyManager mTelephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            //권한 없을 경우
            if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, Manifest.permission.READ_PHONE_STATE)) {
                // 사용자에게 해당 권한이 필요한 이유에 대해 설명
                Toast.makeText(context, "앱 실행을 위해서는 전화 관리 권한을 설정해야 합니다.", Toast.LENGTH_SHORT).show();
            }
            // 최초로 권한을 요청하는 경우
            ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.READ_PHONE_STATE} , 2);
            return null;
        } else {
            Log.v("TAG","Permission is granted");
            if (mTelephony.getDeviceId() != null) {
                deviceId = mTelephony.getDeviceId();
            } else {
                deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
            }
        }
    }
    return deviceId;
}

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case 2: {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(getApplicationContext(), "Permission granted", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(getApplicationContext(), "Permission denied", Toast.LENGTH_SHORT).show();
            }
            return;
        }
    }
}
 


만약 deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 가 제대로 동작하지 않는다면, 다른 대책을 세워야 할 거 같다.


if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        //deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        deviceId = getUniqueID(context);
}


public synchronized static String getUniqueID(Context context) {
    // 자바에서는 기본적으로 UUID를 생성하는 클래스를 지원한다.
    // UUID를 생성하면 간단하게 유니크한 ID를 얻을 수 있다.
    if (uniqueID == null) {
        SharedPreferences sharedPrefs = context.getSharedPreferences(PREF_UNIQUE_ID, Context.MODE_PRIVATE);
        uniqueID = sharedPrefs.getString(PREF_UNIQUE_ID, null);
        if (uniqueID == null) {
            uniqueID = UUID.randomUUID().toString().replace("-", "");
            Log.e("UUID : ",uniqueID);
            SharedPreferences.Editor editor = sharedPrefs.edit();
            editor.putString(PREF_UNIQUE_ID, uniqueID);
            editor.commit();
        }
    }
    // 앱을 삭제하면 정보 삭제되므로 앱 삭제시에는 관리자에게 초기화 요청 필요
    return uniqueID;
}



Eclipse 테스트 일시 : 2020.3.19일

오래전에 Eclipse 개발툴로 개발한 걸 테스트 해봤다.

디바이스를 구분하는 고유 번호로  ANDROID_ID 로 대체하면 해결되더라.

릴리즈 버전과 디버깅 버전 APK 의 Android ID 가 다르지만 사용자에게는 문제는 없다.

//TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
String uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);


블로그 이미지

Link2Me

,
728x90

PHP에서 QR Code를 생성하기 위해서는 php.ini 환경설정 값을 먼저 확인한다.



PHP QRCode 를 다운로드 받는 곳

https://sourceforge.net/projects/phpqrcode/files/releases/ 에서 받는다.

2010년에 구현된 파일로 Class 로 되어 있고 잘 동작된다.


파일을 폴더 그대로 압축을 푼다.


실행 예제 코드

<?php
// 압축을 해제한 phpqrcode 폴더의 qrlib.php 파일을 include한다.
include_once "./phpqrcode/qrlib.php";
$qrContents = "00아파트 122동 703호";
$filePath = sha1($qrContents).".png";

if(!file_exists($filePath)) {
    $ecc = 'H'; // L, M, Q, H, This parameter specifies the error correction capability of QR.
    $pixel_Size = 10; // This specifies the pixel size of QR.
    $frame_Size = 10; // level 1-10
    QRcode::png($qrContents, $filePath, $ecc, $pixel_Size, $frame_Size);
    echo "파일이 정상적으로 생성되었습니다.";
    echo "<hr/>";
} else {
    echo "파일이 이미 생성되어 있습니다.\n파일을 지우고 다시 실행해 보세요.";
    echo "<hr/>";
}

echo "저장된 파일명 : ".$filePath;
echo "<hr/>";
echo "<center><img src='".$filePath."'/></center>"
?>


실행결과



다음은 안드로이드 앱에서 QR Code를 구현하여 카메라로 위 이미지를 인식하면 $qrContents = "00아파트 122동 703호"; 인 00아파트 122동 703호 를 결과로 보여주는지 확인하면 된다.

보통은 https://abc.com/index.php 처럼 상품 설명이나 사이트 이동으로 연결하여 만든다고 보면 된다.



'안드로이드 > Android QRCode' 카테고리의 다른 글

Android QRCode 샘플  (0) 2020.08.25
PHP TCPDF 설치 및 샘플 코드  (0) 2020.05.30
QRCode 개요  (0) 2020.03.03
블로그 이미지

Link2Me

,
728x90

QR Code는 흑백 격자 무늬 패턴으로 정보를 나타내는 매트릭스 형식의 이차원 바코드이다.


QR코드는 1994년 일본 도요타 자동차 자회사인 덴소 웨이브가 도요타 자동차 전용 부품을 구별하기 위해 개발하면서 시작됐다. 기존 바코드 방식이 1차원적인 가로 선만으로는 담을 수 있는 정보의 양이 제한되기 때문에, 일정 면적에 정보를 담을 수 있는 2차원 코드를 개발한 것이다.

기존의 바코드가 20자 내외의 숫자 정보만 저장할 수 있지만, QR코드는 숫자 최대 7,089자, 문자 최대 4,296자를 저장할 수 있다.


QR코드는 정사각형 안 흑백 격자선 위로 다양한 무늬가 입혀진 것이다. QR코드는 크게 3가지 패턴으로 구성된다. 위치 찾기 패턴과 얼라인먼트 패턴, 셀 패턴 이다. 위치 찾기 패턴은 모든 QR코드의 세 모서리에 크게 자리 잡고 있는 사각형이다. 해당 기능은 QR코드를 인식기가 360도 어느 방향에서 감지하더라도 QR코드의 위치를 정확하게 파악, 빠른 정보 탐색이 가능하도록 만드는 일종의 나침반 기능을 한다. 이를 통해 데이터 인식 및 분석 속도가 빨라져 QR코드란 이름도 ‘빠른 응답’(Quick Response)에서 나왔다.


QR코드가 널리 쓰이면서 새로운 문제도 등장하고 있다. QR코드는 바코드에 비해 많은 정보를 담을 수 있어 QR코드에 악성코드나 유해 웹사이트 주소를 담아 유포하는 사례가 많아지고 있다.
검증된 기관이나 기업이 아닌 곳에서 제공하는 QR코드의 경우 접속할 때 신중해야 한다.


스마트폰의 사진촬영 기능으로 QR코드를 찍으면 QR코드에 담긴 정보에 따라 다양한 기능을 할 수 있다.

가장 많이 쓰는 기능은 특정 홈페이지로 보내는 것이다. 가령 제품 상자에 인쇄된 QR코드를 찍는 순간 제품을 자세히 소개하는 홈페이지를 보여주는 식이다. 인쇄매체를 가진 언론사는 기사 옆에 QR코드를 찍어 관련된 동영상을 쉽게 보여줄 수 있다.


Web상에서 QR Code를 생성해주는 사이트(https://ko.qr-code-generator.com/)가 있다.

QR Code를 생성하면 생성된 QR Code 이미지를 다운로드할 수 있고, Android QRCode Sample 코드로 입력한 텍스트가 출력되는지 Scan으로 확인해 볼 수 있다.


QR Code 판독원리  

1. 카메라에서 QR코드 영상 입력  

2. 카메라 영상을 흑백(bitmap)으로 변환.  

3. 흑백영상에서 QR코드 찾기  

4. 찾은 QR코드의 판독  

5. 판독데이터를 스마트폰 화면에 출력


다량의 QR Code를 한꺼번에 등록하는 기능을 하려면 별도로 PHP 라이브러리를 사용하는 것이 좋다.



'안드로이드 > Android QRCode' 카테고리의 다른 글

Android QRCode 샘플  (0) 2020.08.25
PHP TCPDF 설치 및 샘플 코드  (0) 2020.05.30
PHP QR code 생성하는 방법  (0) 2020.03.04
블로그 이미지

Link2Me

,
728x90

안드로이드폰에서 폴더에 있는 text 파일을 읽어서 미리보기하는 기능이 필요하여 구현해봤다.


테스트 환경 : LG G5 테스트 성공, Samsung S10 실패


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">

    <Button
        android:id="@+id/btn_fileread"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_gravity="center"
        android:text="File Read"
        android:textAllCaps="false"/>
</LinearLayout>

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

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/popupTitle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="3"
            android:gravity="center"
            android:text="읽은 파일 미리보기"
            android:textSize="18sp"
            android:textStyle="bold"
            android:background="@color/colorPrimary"/>

        <TextView
            android:id="@+id/popupSaveBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="적용"
            android:textSize="18sp"
            android:textStyle="bold"
            android:background="#B5FFD5"/>

        <TextView
            android:id="@+id/popupCancelBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="취소"
            android:textSize="18sp"
            android:textStyle="bold"
            android:background="#FFFF00"/>

    </LinearLayout>

    <ScrollView
        android:id="@+id/popupScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000000">

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

            <TextView
                android:id="@+id/popupText"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:ellipsize="start"
                android:gravity="left"
                android:textColor="#FFFFFF"
                android:textStyle="bold" />
        </LinearLayout>
    </ScrollView>

</LinearLayout>


갤럭시 S10에서 안되는 이유는 사용된 코드가 오래된 코드라서 그렇더라.

인터넷에서 찾아서 테스트한 코드가 매우 오래된 코드라서 보안이 강화된 폰에서는 읽어내질 못하더라.

주황색으로 표시된 코드를 다른 코드로 찾아서 대체하면 해결될 것이다.

최신폰에서 동작되도록 하는 것은 개발자 각자의 몫으로....

미리 보기를 위한 팝업창 코드에는 전혀 문제가 없다는 것도 확인했다.

에러 때문에 좀 삽질을 하기는 했지만.


public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "MainActivity";
    Context mContext;

    Button popup_alert;
    String select_filePath;
    ScrollView pScrollView;
    View popupView;

    private static final int READ_REQUEST_CODE = 42;

    protected View mPopupView; // 팝업 뷰

    private void initView() {
       // 1. 특정 폴더에 있는 파일 읽어서 미리보기 화면에 보여주기
        popup_alert = findViewById(R.id.btn_fileread);
        popup_alert.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_fileread:
                popupView = view;
                showFileChooser();
                break;
        }
    }

    private void showFileChooser() {
        Intent intent;
        if (android.os.Build.MANUFACTURER.equalsIgnoreCase("samsung")) {
            intent = new Intent("com.sec.android.app.myfiles.PICK_DATA");
            intent.putExtra("CONTENT_TYPE", "*/*");
            intent.addCategory(Intent.CATEGORY_DEFAULT);
        } else {

            String[] mimeTypes =
                    {"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .doc & .docx
                            "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", // .ppt & .pptx
                            "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xls & .xlsx
                            "text/plain",
                            "application/pdf",
                            "application/zip", "application/vnd.android.package-archive"};

            intent = new Intent(Intent.ACTION_GET_CONTENT); // or ACTION_OPEN_DOCUMENT
            intent.setType("*/*");
            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
        }

        try {
            startActivityForResult(Intent.createChooser(intent, "Select a File"), READ_REQUEST_CODE);
        } catch (android.content.ActivityNotFoundException ex) {
            Toast.makeText(this, "Please install a File Manager.",Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case READ_REQUEST_CODE:
                if (resultCode == RESULT_OK) {
                    // Get the Uri of the selected file
                    Uri uri = data.getData();
                    select_filePath = getRealPathFromURI(uri); // 이 함수를 다른 걸로 대체하는게 공부
                    Log.e(TAG, "file_path = " + select_filePath);
                    if(select_filePath != null){
                        mPopupWindowShow(readFile(select_filePath));
                    }
                }
                break;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    private String getRealPathFromURI(Uri uri) {
        String filePath = "";
        filePath = uri.getPath();
        //경로에 /storage가 들어가면 real file path로 판단
        if (filePath.startsWith("/storage"))
            return filePath;

        String wholeID = DocumentsContract.getDocumentId(uri);

        //wholeID는 파일명이 abc.zip이라면 /document/B5D7-1CE9:abc.zip와 같다.
        // Split at colon, use second item in the array
        String id = wholeID.split(":")[1];

        Log.e(TAG, "id = " + id);

        String[] column = { MediaStore.Files.FileColumns.DATA };

        //파일의 이름을 통해 where 조건식을 만든다.
        String sel = MediaStore.Files.FileColumns.DATA + " LIKE '%" + id + "%'";

        //External storage에 있는 파일의 DB를 접근하는 방법이다.
        Cursor cursor = getContentResolver().query(MediaStore.Files.getContentUri("external"),
                column, sel, null, null);

        int columnIndex = cursor.getColumnIndex(column[0]);

        if (cursor.moveToFirst()) {
            filePath = cursor.getString(columnIndex);
        }
        cursor.close();
        return filePath;
    }

    private String readFile(String filepath) {
        File fileEvents = new File(filepath);
        StringBuilder text = new StringBuilder();
        try {
            BufferedReader br = new BufferedReader(new FileReader(fileEvents));
            String line;
            while ((line = br.readLine()) != null) {
                text.append(line);
                text.append('\n');
            }
            br.close();
        } catch (IOException e) { }
        String result = text.toString();
        return result;
    }

    public void mPopupWindowShow(String url) {
        Display display = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        Point point = new Point();
        display.getSize(point);
        int height = (int) (point.y * 0.8);
        int width = (int) (point.x * 0.9); // Display 사이즈의 90%

        // inflate the layout of the popup window
        LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
        LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.popup_window,null);
        mPopupView = layout.findViewById(R.id.linear_layout);

        TextView popupTitle = layout.findViewById(R.id.popupTitle);
        popupTitle.setText("내용 미리보기");
        TextView popupSave = layout.findViewById(R.id.popupSaveBtn);

        TextView popupCancel = layout.findViewById(R.id.popupCancelBtn);

        pScrollView = layout.findViewById(R.id.popupScrollView);
        TextView popupContent = layout.findViewById(R.id.popupText);
        popupContent.setText(url);

        pScrollView.setVerticalScrollBarEnabled(true); // 수직방향 스크롤바 사용 가능하도록 설정
        popupContent.setMovementMethod(new ScrollingMovementMethod());
        popupContent.setTextSize(12);

        // create the popup window
        boolean focusable = true; // lets taps outside the popup also dismiss it
        final PopupWindow popupWindow = new PopupWindow(mPopupView, width, height, focusable);

        popupWindow.showAtLocation(popupView, Gravity.BOTTOM, 0, 0); // Parent View 로부터의 위치

        popupSave.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "적용을 시작합니다.", Toast.LENGTH_SHORT).show();
            }
        });

        popupCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (popupWindow != null && popupWindow.isShowing()){
                    popupWindow.dismiss();
                }
            }
        });
    }

}




블로그 이미지

Link2Me

,
728x90

필요한 걸 찾으려고 하니 어떤 게시글에 적혀 있는지 찾기 어려워서 별도로 적어둔다.


API28(Android 9) 이상을 사용하는 경우에는 추가로 고려할 사항이 있다.


AndroidManifest.xml 파일에 추가할 사항

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.link2me.android.MyPhone">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application
        android:allowBackup="false"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true" 
<!-- http:// 통신일 경우에도 통신 가능하도록 -->

        android:theme="@style/AppTheme.NoActionBar">
        <activity android:name="com.link2me.android.MyPhone.Intro">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"
                tools:replace="android:resource" />
        </provider>
    </application>
</manifest>

Android 7.0(Nougat / API 24)에서 Intent로 URI 파일 경로 전송시 "file://" 노출되어 있으면 FileUriExposedException 오류가 발생하게 되고 앱이 종료된다.
앱간 파일을 공유하려면 "file://" 대신 "content://"로 URI를 보내야 한다.
URI로 데이터를 보내기 위해선 임시 액세스 권한을 부여해야 하고 FileProvider를 이용해야 한다.

그리고 res/xml/provider_paths.xml 생성을 해야 한다.

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="download" path="." />
</paths>




앱 build.gradle 은 androidX 로 변경한다.
오래된 코드를 Android Studio 에 맞게 수정하기 위해서는 이 코드 변환 과정이 필요하다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // implementation 'com.android.support:appcompat-v7:28.0.0'
    //implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
}




블로그 이미지

Link2Me

,
728x90

객체 지향 언어에서는 객체를 생성하기 위해서는 먼저 클래스를 선언하고, 해당 클래스로부터 객체를 생성한다.

하지만, 코틀린에서는 이런 과정을 거치지 않고도 객체를 생성할 수 있다.

object 키워드를 사용하면, 클래스가 아니라 객체 그 자체, 인스턴스가 된다.


코틀린에는 static keyword가 없고 object 키워드를 통해
- 싱글턴을 정의하는 방법
- 동반객체 companion object를 이용한 팩토리 메서드 구현
- 무명클래스(익명 클래스)의 선언
를 한다.


예제1

fun main(){
    // Person은 그 자체로 하나의 객체이기 때문에, 바로 .을 찍어 프로퍼티나 메서드에 접근할 수 있다.
    Person.name = "홍길동"
    Person.age = 30
    println("이름 : ${Person.name}, 나이 : ${Person.age}")
}

// object 는 subclass 의 명시적인 선언 없이 객체 생성
object Person {
    var name: String = ""
    var age: Int = 0
}
 


예제2

- object 키워드는 어떤 객체가 반드시 하나만 존재해야 할 때, 즉 싱글톤 패턴에 활용할 수 있다.

// 예제 2.1

fun main(args: Array<String>) {
    Singleton.printVarName()

    println("\nSingleton variableName Change")
    Singleton.variableName = "강감찬"
    var a = A()
}

object Singleton{
    init {
        println("Singleton class invoked.")
    }
    var variableName = "홍길동"
    fun printVarName(){
        println("변수명 : $variableName ")
    }
}

class A {
    init {
        println("Class init method. Singleton variableName property : ${Singleton.variableName}")
        Singleton.printVarName()
    }
}

// 예제 2.2
fun main(args: Array<String>) {
    var a = A()
    a.printVarName()

    println()
    Singleton.printVarName()
}

open class A {
    open fun printVarName() {
        println("I am in class printVarName")
    }

    init {
        println("I am in init of A")
    }
}

object Singleton : A() {
    init {
        println("Singleton class invoked.")
    }

    var variableName = "홍길동"
    override fun printVarName() {
        println(variableName)
    }
}



예제3. companion object

- 동반 객체(companion object)는 최상위 수준에서는 사용할 수 없고 클래스 내부에 정의하여 사용한다.

  동반 객체는 클래스 내부에 정의된 객체 선언이라고 할 수 있다.

  하나의 클래스에는 하나의 동반 객체만 포함될 수 있다.

- 동반 객체를 포함하는 클래스 내부에서는 동반 객체의 속성이나 함수를 자신의 속성이나 함수인 것처럼 인식하므로 동반 객체의 이름을 지정하지 않고 사용할 수 있다.

- 카 판매 버튼을 클릭할 때마다 카운트가 증가되는 함수라고 생각하면 된다.

- companion object 가 서로 다른 객체(아우디, 그랜저)의 카운트 총합을 자동 합산한다.

fun main(args: Array<String>){
    var car1 = CarSales("아우디")
    var car2 = CarSales("그랜저")

    car1.sales()
    car1.sales()
    car1.sales()
    car1.sales()

    car2.sales()
    car2.sales()

    println("${car1.name} : ${car1.count}")
    println("${car2.name} : ${car2.count}")
    println("총계 : ${CarSales.total}")
 
}

// object 는 subclass 의 명시적인 선언 없이 객체 생성

class CarSales(val name: String){
    // 코틀린에는 정적(static) 변수 혹은 메소드가 없고,
    // 대신 패키지 내에 함수를 선언하여 사용할 수 있다.
    companion object {
        var total = 0
    }

    var count = 0
    fun sales(){
        total++
        count++
    }
}


예제4

fun main(args: Array<String>){
    var first = MyHtml.generateHtmlObject("first")
    first.setColumnHeaders("1열","2열","3열")
    //println("first.generateTable() => ${first.generateTable()}")

    val second = MyHtml.generateHtmlObject("second", 5,5)
    second.setColumnHeaders("월","화","수","목","금")

    val third = MyHtml.generateHtmlObject("third",2,7)
    third.setColumnHeaders("적색","녹색","노랑","파랑","보라","검정","주황")

    println("생성된 table 개수 : ${MyRecord.showTableCount()}")
    for( (k,v) in MyRecord.showTable()) {
        println("\n <!-- $k table (${v.row} x ${v.col}) -->")
        println(v.generateTable())
        println()
    }
}

object MyRecord {
    private var _count = 0
    private val _tables = hashMapOf<String,MyHtml>()

    fun gen(table: MyHtml){
        _tables.put(table.name, table)
        _count++
    }
    fun showTableCount() = _count
    fun showTable(): Map<String,MyHtml> = _tables
}

class MyHtml {
    val name: String
    val row: Int
    val col: Int
    private val HEAD = "<table border='1' cell-spacing='10' cell-padding='10'"
    private val TAIL = "</table>"
    private lateinit var comment: String
    private val tableColumns = ArrayList<String>()

    private constructor(_name: String, row: Int, col: Int ){
        this.name = _name
        this.row = row
        this.col = col
    }

    fun setColumnHeaders(vararg colNames: String){
        this.tableColumns.addAll(colNames)
    }

    companion object _GenTable {
        fun generateHtmlObject(_name: String, row: Int = 2, col: Int = 3): MyHtml {
            val mh = MyHtml(_name, row, col)
            mh.comment = "<!-- auto-generator table by MyHtml v1.0 -->"
            // 테이블 객체 기록
            MyRecord.gen(mh)
            return mh
        }
    }

    private inner class Tr {
        val rowHtml = """
            |<tr>
            | ${"<td>-</td>".repeat(col)}
            |</tr>${"\n"}
        """.trimMargin()
    }

    private inner class Th(val wpx: Int = 40) {
        val headers = tableColumns.map {
            "<th width='$wpx'> $it </th>"
        }
        val rowHtml = """
            |<tr>
            | ${headers.joinToString("")}
            |</tr>
        """.trimMargin()
    }

    // 테이블 생성 실행
    fun generateTable() =
        """
                |$comment
                |$HEAD
                |${Th().rowHtml}
                |${Tr().rowHtml.repeat(row)}
                |$TAIL
            """.trimMargin()
}


블로그 이미지

Link2Me

,
728x90

코틀린에서는 접근자와 설정자를 따로 만들어줄 필요가 없다.
자바에서는 private 접근제한자로 은닉화된 멤버 변수에 getter/setter 메소드를 사용해서 접근한다.
코틀린은 property가 getter/setter를 대체한다.

코틀린에서 속성(property)란
최상위 변수(함수나 클래스 외부에 정의된 변수)나 클래스의 멤버 변수로 선언하면 속성으로 간주된다.
클래스의 멤버 변수는 모두 private 제한자로 지정된다.
따라서 해당 클래스의 내부의 getter와 setter를 통해서만 속성을 참조할 수 있다.

 

package com.myapp.funcargs

fun main(){
    val a = Person("홍길동", 30)
    println(a)
    //println(a.toString())

    val b = a.copy("이순신")
    println(b)

    val c = b.copy(age=40)
    println(c)

    val(name, age) = Person("강감찬",35)
    println("성명 : $name, 나이 : $age")



    val obj = Pair<String, Int>("김좌진", 50)
    println(obj)
}

// 데이터는 보유하지만 아무것도 안하는 클래스
data class Person(val name: String, val age: Int)
// 파라미터 없는 기본 생성자가 필요한 경우에는
// data class Person(val name: String = "", val age: Int = 0)
// 모든 프로퍼티에 기본 값을 설정해 주면 된다.
// 기본 생성자에서 선언된 속성을 통해, 컴파일러가 자동으로 toString(), hashCode(), equals() 등을 생성한다.
// 명시적으로 정의해준 경우에는 컴파일러가 자동으로 생성하지 않는다.

 

실행결과

 

 

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
 
@Parcelize
data class Memo_AddEdit (
    var idx: String,
    var memo: String,
    var userID: String,
    var userNM: String,
): Parcelable
 
 
class MainActivity : AppCompatActivity() {
    private lateinit var mContext: Context
    private lateinit var mMemo_AddEdit: Memo_AddEdit
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mContext = this@MainActivity
 
        initView()
    }
 
    private fun initView() {
        // 값을 할당함으로써 초기화가 된다.
       mMemo_AddEdit = Memo_AddEdit("0","",userid,userNM)
 
    }
}

 

'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 null 안전과 예외  (0) 2020.05.05
코틀린 object  (0) 2020.01.19
코틀린 고차함수와 람다 그리고 확장함수  (0) 2020.01.14
코틀린 접근 제한자  (0) 2020.01.14
코틀린 패키지  (0) 2020.01.13
블로그 이미지

Link2Me

,
728x90
코틀린 함수의 기본형태
fun 함수명(매개변수):Unit {
}

fun 함수명(매개변수):리턴타입 {
   return 값
}


고차함수

고차함수(High-order Function)란 다른 함수를 인자로 사용하거나, 함수를 결과값으로 반환하는 함수를 말한다.


package com.myapp.funcargs

fun main(){
    val a = sum(4,5) // 일반 인자
    println("a result : $a")
    val b = mul(sum(1,2), 3) // 인자에 함수 사용
    println("b result : $b")

    println("ResultFun : ${resultFun()}")
}

fun sum(a: Int, b: Int) = a + b
fun mul(a: Int, b: Int) = a * b
fun resultFun(): Int {
    return mul(3,5) // 함수의 반환 값으로 함수 사용
}


람다 표현식

람다식, 또는 람다 함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수 (Anonymous Functions)를 지칭하는 용어이다.


val lambdaName: Type = {argument -> codeBody}


람다 함수는 { } 안에 매개변수와 함수 내용을 선언하는 함수로 다음 규칙에 따라 정의한다.
- 람다 함수는 항상 { }으로 감싸서 표현해야 한다.
- { } 안에 -> 표시가 있으며 -> 왼쪽은 매개변수, 오른쪽은 함수 내용이다.
- 매개변수 타입을 선언해야 하며, 추론할 수 있을 때는 생략할 수 있다.
- 함수의 반환값은 함수 내용의 마지막 표현식이다.


fun sum(a: Int, b: Int) = a + b
를 람다 함수로 정의하면
val sum ={a: Int, b: Int -> a + b}

또는

val sum: (Int, Int) -> Int ={a, b -> a + b}


package com.myapp.funcargs

fun main(){
    val a = sum(4,5) // 일반 인자
    println("a result : $a")
    val b = mul(sum(1,2), 3) // 인자에 함수 사용
    println("b result : $b")

    println("ResultFun : ${resultFun()}")

    val c:(String)->Unit ={str -> println("$str 람다함수")}
    b(c)
}

val sum ={a: Int, b: Int -> a + b}
// val sum: (Int, Int) -> Int ={a, b -> a + b}
// val mul ={a: Int, b: Int -> a * b}
val mul: (Int, Int) -> Int ={a, b -> a * b}
fun resultFun(): Int {
    return mul(3,5) // 함수의 반환 값으로 함수 사용
}
fun b (function: (String)->Unit){
    function("b가 호출한")
}




확장함수(Extension Function)
Extension functions(확장 함수)는 기존에 정의된 클래스에 함수를 추가하는 기능이다.
자신이 만든 클래스는 새로운 함수가 필요할 때 쉽게 추가할 수 있다.
하지만 Standard Library 또는 다른 사람이 만든 라이브러리를 사용할 때 함수를 추가하기가 매우 어렵다.

확장 함수는 fun 클래스이름.함수이름(매개변수): 리턴타입 { 구현부 }으로 정의할 수 있다.

또는
fun 확장하려는대상.함수명(매개변수):Unit {
}

fun 확장하려는대상.함수명(매개변수):리턴타입 {
   return 값
}


확장함수 예제1

class Car {
    fun getPrice() : Int {
        return 8000
    }
}

fun Car.getBrandName() : String {
    return "BMW"
}

fun Car.getBrandName2() : Car {
    println("KIA")
    return this
}

fun main(args: Array<String>) {
    val car = Car()
    println(car.getBrandName())
    println(car.getBrandName2().getPrice())


확장함수 예제2

fun String.getLonggerLength(x: Int) : Int {
    return if(this.length > x) this.length else x
}

fun main(args: Array<String>) {
    println("Hello".getLonggerLength(3))
} 

fun main(args: Array<String>) {
    println("Hello".getLonggerStringLength("Have a nice day!"))
}

fun String.getLonggerStringLength(x: String) : Int {
    return if(this.length > x.length) this.length else x.length
}


확장함수 예제3

class Car {
    fun getPrice(): Int {
        return 10000
    }
}

fun Car.getPrice(): Int {
    return 20000
}

fun Car.getPrice(price: Int): Int {
    return 30000
}

fun main(args: Array<String>) {
    val car = Car()
    println(car.getPrice())
    println(car.getPrice(0))


확장함수 예제4

fun List<Int>.getHigherThan(num: Int): List<Int> {
    val result = arrayListOf<Int>()
    for (item in this) {
        if (item > num) {
            result.add(item)
        }
    }
    return result
}

fun main() {
    val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6, 7)
    val filtered = numbers.getHigherThan(3).toString()
    System.out.println(filtered)
}
 


'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 object  (0) 2020.01.19
코틀린 data class (자바의 getter 와 setter 클래스)  (0) 2020.01.16
코틀린 접근 제한자  (0) 2020.01.14
코틀린 패키지  (0) 2020.01.13
코틀린 인터페이스  (0) 2020.01.13
블로그 이미지

Link2Me

,
728x90

접근 제한자란 객체가 공개되어야 하는 범위를 정해주는 역할을 한다.

자바에서는 접근 제한자(Access Modifier)라는 용어를 사용하고
코틀린에서는 Visibility Modifier로 사용하는데, 우리말로는 접근 제한자 또는 가시성 제한자라고 할 수 있다.
코틀린에서 접근 제한자를 가질 수 있는 요소로는 class, object, interface, constructor, function, property 등이 있다.

자바에서는 접근 제한자를 붙이지 않으면 default(package-private) 이다.
그러나 코틀린에서는 아무것도 붙이지 않으면 public 이다.

코틀린에서 말하는 모듈은 자바와는 약간의 차이가 있는데, 자바에서의 모듈은 기능이 비슷한 클래스들의 집합체를, 코틀린에서는 동일한 컴파일의 집단을 의미한다.


패키지 레벨의 접근제한자

프로퍼티와 함수를 최상위(Top) 레벨에 작성한다는 것은 클래스에 멤버로 포함하지 않고 코틀린 파일에 정의하는 것을 의미한다.
package foo

public val myData: Int = 10
public fun myFun() {}
public class myClass() {}

- 접근 제한자를 표기하지 않으면 public이 기본으로 설정되며, 누구나 접근이 가능하다.

- private : 해당 .kt 파일 내부에서만 사용 가능하다.
- internal : 프로젝트의 모듈 안에서 누구나 접근이 가능하다.
- protected : 최상위(top-level)에서는 사용할 수 없다.
 

클래스와 인터페이스의 접근제한자
클래스 또는 인터페이스 안에 선언되는 멤버들에 사용되는 접근 제한자는 다음과 같은 의미를 갖는다.
- public : 누구나 접근이 가능하다.
- private : 클래스 내부에서만 접근이 가능하다.
- protected : 클래스 내부와 상속받는 객체에서 접근이 가능하다.
- internal : 프로젝트의 모듈 안의 누구나 접근이 가능하다.


private, protected, public 과 Java와 코틀린에서 같은 의미로 사용된다.
하지만 internal은
자바에서는 default(같은 패키지내에서만 접근가능)
코틀린에서는 같은 모듈안에서만 사용 가능하다. 


생성자의 접근제한자
생성자에도 접근제한자를 붙일 수 있다. 기본은 public 생성자로 누구나 접근이 가능하다.
internal을 붙이면 모듈 내에서만 접근 가능하다.
private을 붙이면 클래스 내에서만 접근이 가능하여 외부에서 생성할 수 없다.

// private 생성자
class Person private constructor(a: Int) {
}



Java 접근 제한자

접근 제한자는 Class 의 멤버에 대해 다른 Class 에서 접근하는 것을 제한하는 용도로 사용된다.

- public : 모든 Class 에서 접근 가능

- protected : 같은 패키지에 있는 Class, 그리고 다른 패키지의 자손 클래스에서 접근이 가능

- default : 같은 패키지에 있는 Class 들만 접근 가능

  String name; // 아무런 접근 제한자도 지정되지 않았으므로 default 접근 제한자다.

- private : 같은 Class 내에서만 접근 가능


지시자

클래스 내부

동일 패키지

상속받은 클래스

이외의 영역

 private


 X

X

X

 default


X

X

 protected


X

 public




블로그 이미지

Link2Me

,
728x90

코틀린에서 프로젝트(Project)는 모듈(Module), 패키지(Package), 파일(File)로 구성되어 있다.
프로젝트에는 모듈이 있고 모듈은 다시 패키지로 구성되어 있다.


Java는 파일명과 클래스명이 동일해야 하지만, 코틀린은 public 클래스는 하나만 사용 해야하는 규칙이 없다.


코틀린 파일은 .kt 확장자를 가지며 맨 위에는 이 파일이 어떤 패키지에 포함된 것인지 코틀린 컴파일러가 알 수 있도록 패키지 이름을 선언해야 한다.
만약 패키지 이름을 선언하지 않으면 그 파일은 자동으로 default 패키지에 포함된다.
파일이 패키지 안에 들어있어도 패키지 이름을 선언하지 않으면 default 패키지에 포함된 것으로 인식한다.
파일에 클래스가 여러 개 정의되어 있다면 파일은 단순히 클래스를 묶는 역할을 하고 kt 확장자가 붙게 된다.
코틀린에서는 파일명과 클래스의 선언 개수에 큰 의미를 두지는 않는다.
같은 파일에 있는 여러 개의 클래스는 모두 그 파일에서 지정한 패키지로 인식한다.


패키지 이름은 파일 맨 위에 적는다. 이때 패키지 이름 앞에 package라는 키워드를 함께 입력해야 패키지 이름으로 인식한다. 단, 패키지의 이름은 특수 문자나 숫자로 시작하면 안된다.

블로그 이미지

Link2Me

,
728x90

코틀린 인터페이스

- Java 와 달리 인터페이스 함수가 구현부를 가질 수도 있고, 추상함수만 가질 수도 있다.

- 상속과 마찬가지로 콜론(:)을 사용해서 인터페이스를 구현한다.

  자바에서는 클래스의 상속과 인터페이스의 구현을 extends 와 implements로 구분한다.

- 상속은 한번에 하나밖에 안되지만, 인터페이스는 콤마(,)로 여러개를 구현 가능하다.

   또한, 콜론(:)으로 동시에 클래스 상속과 인터페이스 구현이 가능하다.

- 코틀린에서는 상속받거나 구현한 함수의 앞에 무조건 override 키워드를 붙이도록 강제한다.

  Java에서는 부모 클래스의 메서드를 재정의하거나 인터페이스 구현 메서드를 @Override 어노테이션으로 구분한다.
  하지만 @Override 어노테이션 추가는 선택 사항이라 메서드의 유형을 코드만으로 구분하기는 어렵다.


fun main(args: Array<String>) {
    var a = Person()
    a.click()
    a.run()
}

interface Clickable {
    // 일반함수 : open 함수로 간주한다.
    fun click() {
        println("버튼을 눌렀어요.")
    }
}

interface Runner {
    // 추상함수 : abstract 함수로 간주한다.
    fun run()
}

class Person : Runner, Clickable {
    override fun run(){
        println("성큼 성큼 달린다.")
    }
} 


만약, 인터페이스 내의 함수들이 구현체가 있고, 중복된 함수가 있을 경우에는 어떻게 할까?

필요한 클래스의 super 를 호출할 수 있다. 둘 다 혹은 필요한 클래스의 함수만 호출해도 된다.


fun main(args: Array<String>) {
    var a = Person()
    a.click()
    a.run()
}

interface Clickable {
    // 일반함수 : open 함수로 간주한다.
    fun click() {
        println("클릭 버튼을 눌렀어요.")
    }
}

interface Runner {
    // 추상함수 : abstract 함수로 간주한다.
    fun run()
    fun click() {
        println("Runner 버튼을 눌렀어요.")
    }
}

class Person : Runner, Clickable {
    override fun run(){
        println("성큼 성큼 달린다.")
    }

    override fun click() {
        super<Clickable>.click()
        super<Runner>.click()
    }
}
 


'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 접근 제한자  (0) 2020.01.14
코틀린 패키지  (0) 2020.01.13
코틀린 추상클래스  (0) 2020.01.12
코틀린(Kotlin) 클래스 선언 및 상속  (0) 2020.01.12
코틀린 기본 이해  (0) 2020.01.07
블로그 이미지

Link2Me

,
728x90

추상 클래스

- 클래스와 일부 멤버가 abstract로 선언되면 추상 클래스이다.
- abstract 키워드 사용시 클래스나 함수에 open 키워드를 따로 명시하지 않아도 된다.
- abstract 함수는 선언부분만 있고 구현부가 없다.

  추상 함수는 상속받은 서브 클래스에서 override하여 재정의해야 한다.
- 다중 상속은 불가능하다.


※ 추상 클래스는 미완성 설계도이다. 미완성 설계도로는 제품을 만들 수가 없다.

   클래스는 직접 객체 생성(인스턴스화)을 할 수 있지만, 추상 클래스는 직접 객체 생성을 할 수가 없다.

   반드시 상속을 받는 자식 클래스가 있어야만 비로소 객체를 생성할 수 있다.


fun main() {
    var b = Dog("불독", 5)

    b.introduce()
    b.bark()
    b.sniff()

    var c = Cat("나갈래", 4)
    c.introduce()
    c.meow()
    c.sniff()
}

abstract class Animal(var name:String, var age:Int, var type:String){
    abstract fun introduce()
    fun sniff(){
        println("냄새를 맡느라고 끙끙거린다.")
    }
}

class Dog(name:String, age:Int) : Animal (name,age,"개"){
    override fun introduce(){
        println("타입 : Dog, ${name}, ${age}살 입니다.")
    }

    fun bark(){
        println("멍멍멍")
    }
}

class Cat(name: String, age: Int) : Animal(name, age, "고양이"){
    override fun introduce(){
        println("타입 : Cat, ${name}, ${age}살 입니다.")
    }

    fun meow() {
        println("야옹야옹")
    }
}


예제2) 출처 : https://www.programiz.com/kotlin-programming/abstract-class

abstract class Person(name: String) {
init {
println("My name is $name.")
}
// 일반 메서드
fun displaySSN(ssn: Int) {
println("My SSN is $ssn.")
}
// 추상 메서드
abstract fun displayJob(description: String)
}

class Teacher(name: String): Person(name) {
// 추상 메서드 구현
override fun displayJob(description: String) {
println(description)
}
}

fun main(args: Array<String>) {
val jack = Teacher("Jack Smith")
jack.displayJob("I'm a mathematics teacher.")
jack.displaySSN(23123)
}


'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 접근 제한자  (0) 2020.01.14
코틀린 패키지  (0) 2020.01.13
코틀린 인터페이스  (0) 2020.01.13
코틀린(Kotlin) 클래스 선언 및 상속  (0) 2020.01.12
코틀린 기본 이해  (0) 2020.01.07
블로그 이미지

Link2Me

,
728x90

코틀린 클래스 선언

class 클래스명(변수) { }

코틀린은 자바, C++과는 달리 클래스 선언 부분에서 이름에 괄호를 붙여 생성자를 만들 수 있다.

자바는 멤버변수를 field라고 하지만 코틀린 프로퍼티라는 용어를 사용한다.
자바는 생성자를 꼭 정의해야 하지만 코틀린은 생략이 가능하다.

코틀린에는 주 생성자와(primary constructor)와 부생성자가(secondary constructor)가 존재한다.
주 생성자는 class 선언과 함께 선언하고, 부 생성자는 추가적인 생성자가 필요할 때 사용한다.
생성자를 정의하지 않으면 기본으로 인자가 없는 생성자를 만들어 준다.

부모의 생성자는 super(), 클래스 내 생성자중 다른 생성자는 this()로 호출이 가능하다.


fun main() {
    var a = Person("박보영", 1990)  // 코틀린은 new 키워드 없이 인스턴스를 생성한다.
    var b = Person("전영경",1997)
    var c = Person("차은우")
    var d = Person("홍길동")

    //println("안녕하세요, ${a.birthYear}년생 ${a.name} 입니다.")
}

class Person (var name:String, val birthYear:Int) {

    // var name:String, val birthYear:Int

    // 프로퍼티 선언 및 초기화는 기본 생성자에서 간결한 구분으로 사용 가능


    // 초기화
    init {

        // init 안에서는 자바처럼 [this.클래스변수= 매개변수]를 얼마든지 할 수 있다.

        println("안녕하세요, ${this.birthYear}년생 ${this.name}입니다.")
    }

    // 보조 생성자 : constructor 키워드로 선언하고 클래스 body에 구현한다.

    // 클래스 별로 여러 개를 가질 수 있다.
    constructor(name: String) : this(name,1997)
}
 

기본 생성자

- 기본 생성자에 어노테이션 접근지정자 등이 있을 경우 constructor 키워드가 필요하다.

  class Person public @Inject constructor(name: String) { .... }



클래스 상속
- 코틀린의 최상위 클래스는 Any 이다.
  Any는 java.lang.Object 와는 다른 클래스이며, equals(), hasCode(), toString()만 있다.
- 상속 키워드는 콜론(:)이다. Java 의 상속 키워드는 extends
- 코틀린의 클래스와 메소드는 기본적으로 final이다. 즉 기본적으로 상속을 못하게끔 되어 있다.
  Java에서는 클래스나 메서드에 final 키워드를 붙여 클래스를 더 이상 상속받지 못하게 하거나,
  메서드를 재정의하지 못하게 할 수 있다.
- 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
  open 변경자가 없으면 final 로 자식 클래스에서 override가 불가능하다. 
  오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야 한다.
- 부모 클래스의 메소드를 자식 클래스에서 오버라이드 한 경우, 해당 메소드는 기본적으로 열려있다.

fun main() {
    var a = Animal("불독", 3,"개")
    var b = Dog("불독", 5)

    a.introduce()
    b.introduce()
    b.bark()

    var c = Cat("나갈래", 4)
    c.introduce()
    c.meow()
}

open class Animal(var name:String, var age:Int, var type:String){
        open fun introduce(){ // open 이므로 override 가능하다.
        println("저는 ${type} : ${name}, ${age} 살 입니다.")
    }
}

class Dog(name:String, age:Int) : Animal (name,age,"개"){
    // 상위 클래스 함수에 open 키워드를 명시하고, 서브 클래스 함수에 override 를 붙인다.
    override fun introduce(){
        println("타입 : 개, ${name}, ${age} 살 입니다.")
    }

    fun bark(){
        println("멍멍멍")
    }
}

class Cat(name: String, age: Int) : Animal(name, age, "고양이"){
    fun meow() {
        println("야옹야옹")
    }
}
 


'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 접근 제한자  (0) 2020.01.14
코틀린 패키지  (0) 2020.01.13
코틀린 인터페이스  (0) 2020.01.13
코틀린 추상클래스  (0) 2020.01.12
코틀린 기본 이해  (0) 2020.01.07
블로그 이미지

Link2Me

,
728x90

구글이 2017년 5월 18일 안드로이드 공식 언어로 코틀린(Kotlin) 추가해서 현재 Android 개발은 Java 와 Kotlin 이 공존하고 있는데, 향후에는 Kotlin 으로 전부 대체될 것으로 보인다.

GitHub 에서 source code를 검색하면 Kotlin 으로 된 코드들이 검색되어 안배우면 안되겠다 싶어서 동영상 보면서 정리를 할 생각이다. 배울 언어가 너무 많아서 안배우고 싶은데, 안배우면 도태될 거 같아서....

 

Kotlin 은 IntelliJ IDE를 만든 JetBrains 에서 개발한 오픈소스 언이이다.

JVM에서 동작하고 Java 와 100% 호환된다.

Java 코드를 Kotlin 에서 인식하고, Kotlin 코드를 Java 에서 인식한다.

 

- Java 플랫폼에서 숫자형은 JVM primitive type 으로 저장됨
- Nullable 이나 제네릭의 경우에는 박싱됨
- 박싱된 경우 identity를 유지하지 않음.
- 문자(Characters) : 숫자로 취급하지 않음.

 

개발툴 다운로드

- 온라인에서 코드 테스트 : https://play.kotlinlang.org/ 에서 간단한 코드를 연습할 수 있다.

- Intellij IDEA 개발툴 : https://www.jetbrains.com/ko-kr/idea/ 에서 community 무료버전을 다운로드 받아서 설치한다.

  Java 에 대한 코드 연습을 Eclipse 툴에서 하듯이

  유투브 강좌 등에서 설명하는 코틀린 문법 연습은 IntelliJ IDEA 툴에서 하면 된다.

  Android Studio 에서 코틀린 선택하여 코드 연습하는 것은 적절하지 않다.

TOOL을 Jetbrains 에서 만들어서 Android Studio 와 메뉴구조는 비슷하다.

잠깐 써본 PHPStorm 도 메뉴구조가 거의 동일하더라.

 

변수의 선언

Java에서는 변수를 선언할 때 타입이 맨 앞에 온다. 코틀린에서는 타입 지정을 생략하는 경우가 흔하다.
Java에서는 두가지종류의 변수가 있는데 하나는 primitive type(기본형)이 있고 다른 하나는 레퍼런스 타입(객체형)이 있다. 코틀린에서는 이 모든게 통합됬다. 즉 레퍼런스 타입밖에 없다.
 

Java에서는 기본이 변수고 상수는 final을 붙혀서 표시했다.

코틀린에서는 변수는 var로 상수는 val로 표시한다.

val // 변경 불가능한 참조를 저장하는 변수다. 선언시에만 초기화 가능. 중간에 값을 변경할 수 없다.

var // 변경 가능한 참조다. 일반적으로 통용되는 변수, 언제든지 읽기쓰기가 가능하다.

 

 

기본적으로 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때에만 var 변수로 변경하라.

코틀린은 문장의 끝에 세미콜론을 사용하지 않는다.

코틀린은 기본변수에서 null 을 허용하지 않으며, 또한 변수에 값을 할당하지 않은채로 사용하게 되면 문법 에러를 표시하고 컴파일을 막아주므로 의도치 않은 동작이나 null pointer exception 등을 원천적으로 차단해 준다는 장점이 있다.

 

var a = 100

또는

var a: Int  // 초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야 한다.

a = 100

 

※ var 키워드를 사용하면 변수의 값은 변경할 수 있지만 변수의 타입은 고정돼 바뀌지 않는다.

var a = 100

a = "kotlin"  ← 컴파일 오류 발생

컴파일러는 변수 선언 시점의 초기화 식으로부터 변수의 타입을 추론한다.

 

var a: Int? // 코틀린에서 null 값을 허용하려면 자료형의 오른쪽에 ? 기호를 붙여준다.

 

문자형 : 코틀린은 문자열을 유니코드 인코딩 방식 UTF-16 BE 을 사용하여 글자 하나당 2bytes 메모리 공간을 차지한다.

var a = '가' // 문자 한글자는 작은 따옴표 사용한다.

var a ="홍길동" // 문자열을 쌍따옴표를 사용한다.

var a ="""여러줄의

   문자열"""  // 쌍따옴표 3개를 사용하면 여러줄 문자열 가능하며, 줄바꿈이나 특수문자까지 사용 가능하다.

 

lateinit 키워드를 사용한 늦은 초기화

코틀린에서는 클래스의 변수(var, val로 선언되는 변수들)를 프로퍼티(Property)라 부른다.

코틀린에서는 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다.

프로퍼티 타입이 null이 될 수 없는 타입이라면 반드시 null이 아닌 값으로 그 프로퍼티를 초기화해야 한다.

private var backPressHandler: BackPressHandler? = null

// null로 초기화하기 위해 null이 될 수 있는 타입인 프로퍼티를 선언한다.

backPressHandler!!.onBackPressed()

// 반드시 null 가능성을 신경써야 한다. non-null 단언 연산자 !!를 꼭 써야 한다.

 

lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다.

private lateinit var backPressHandler: BackPressHandler

// 초기화를 하지 않고 null이될 수 없는 프로퍼티를 선언한다.

backPressHandler.onBackPressed()

 

lateinit은 다음 조건에서만 사용할 수 있다.
1) var 변수에서만 사용한다.
2) null 값으로 초기화 할 수 없다.
3) 초기화 전에는 변수를 사용할 수 없다.
4) Int, Long, Double, Float에는 사용할 수 없다.

5) lateinit은 custom getter/setter를 사용하지 않은 프로퍼티에만 사용할 수 있다.

문자열 접합 연산

println("안녕하세요, $name!")  → Java의 문자열 접합 연산 ("안녕하세요," + name + "!")와 동일한 기능

 

존재하지 않는 변수를 문자열 템플릿 안에서 사용하면 컴파일 오류가 발생한다.

$ 문자를 문자열에 넣고 싶다면 println("\$x")와 같이 \를 사용해 $를 이스케이프 시켜야 한다.

${name} 처럼 중괄호로 변수명을 감싸는 습관을 들이면 좋다. 이렇게 하면 복잡한 식도 중괄호로 둘러싸서 문자열 템플릿안에 넣을 수 있다.

 

 

형변환

코틀린에서는 Int 형과 Long 형 타입이 다른 경우 자동으로 형변환이 되지 않는다

즉, 다른 언어들이 지원하는 암시적 형변환은 지원하지 않고, 명시적 형변환만 지원한다.
  var a: Int = 100
  var b: Long = a.toLong()

 

배열

배열은 Array 클래스로 표현된다.

var intArr = arrayOf(1, 2, 3, 4, 5)  // arrayOf 함수를 통해 배열에 저장할 값들을 나열한다.

var intArr = arrayOfNulls<Int>(10) // null 로 채워진 배열. 

 

함수

function 의 약자인 fun 을 사용한다.

 

리턴과 파라미터가 없는 함수
fun 함수이름() { }

값을 리턴 하는 함수
fun 함수이름() : 리턴타입 { return 값 }

리턴과 파라미터가 있는 함수
fun 함수이름( 변수이름: 변수타입) : 리턴타입 { return 값 }

fun add(a: Int, b: Int, c: Int): Int {   // 반환형

    return a + b + c

}

단일표현식 함수 

fun add(a: Int, b: Int, c: Int) = a + b + c  // 반환형의 타입 추론이 가능하여 반환형을 생략할 수 있다.

 

IF문

- val max = if( a > b) a else b

  Java 의 삼항연산자(ternary)가 코틀린에는 없음.

  int max = (a > b) > a : b; // Java 의 삼항연산자

- If식의 branches 들이 블록을 가질 수 있음 {  }

  val max = if(a > b) {

    print("Choose a")

    a

  } else {

    print("Choose b")

    b

  }

 

다중 조건문 when

- when문은 C계열 언어의 switch 문에 대응한다.

- when문은 각각의 branches의 조건문이 만족할 때까지 위헤서부터 순차적으로 인자를 비교함.

  when (x) {

     1 -> print("x == 1")

     2 -> print("x == 2")

     else ->{

           print("x is neither 1 nor 2)

     }

  }

- when이 식으로 사용된 경우 else 문이 필수임

- when 이 식으로 사용된 경우 컴파일러가 else문이 없어도 된다고 입증할 수 있는 경우에는 else 생략 가능

  var res = when (x) {

    true -> "맞다"

    false -> "틀리다"

  }

- branch의 조건문에 콤마를 사용하여 여러조건들을 처리할 수 있다.

  when (x) {

     0, 1 -> print("x == o or x == 1")

     else -> print("기타")

  }

- branch의 조건문에 함수나 식을 사용할 수 있다.

  when (x) {

     parseInt(x) -> print("s encodes x ")

     1 + 5 -> print("6")

     else -> print("s does not encode x")

  }

- range 나 collection 에 in 이나 !in 으로 범위 등을 검사할 수 있다.

  val validNumbers = listOf(3, 6, 9)

  when (x) {

    in validNumbers -> print("x is valid")

    in 1..10 -> print("x is in the range")

    !in 10..20 -> print("x is outside the range")

    else -> print("none of the above")

  }

- is 나 !is 를 이용하여 타입도 검사할 수 있다.

  fun hasPrefix(x: Any) = when (x) {

is String -> x.startsWith("prefix")

else -> false

  }

- when 에 인자가 없으면, 논리연산으로 처리된다.

  when {

x.isOdd() -> print("x is odd")

x.isEven() -> print("x is even")

  }

 

For Loops

- for문은 iterator 를 제공하는 모든 것을 반복할 수 있다

  for(item in collection)

      print(item)

- for문의 body가 블록이 올 수도 있다.

  for(item in collection) {

     print(item.id)

     print(item.name)

  }

- for(i in 0..9)  // 0부터 1씩 증가시키며 반복한다.

  for(i in 0..9 step 3)  

     print(i)    // 3씩 증가되어 0, 3, 6, 9 를 출력한다.

- 감소 반복

  for(i in 9 downTo 0)   // 9부터 1씩 감소하여 반복한다.

  for(i in 9 downTo 0 step 3)  // 9부터 3씩 감소하여 반복한다.

- 문자 for 문

  for(i in 'a'..'f')   // a부터 f까지 반복한다.

- 반복문 탈출 break

  loop@for(i in 1..10){

     for(j in 1..5) {

         if(i == 1 && j == 2) break@loop

         println("i : $i, j : $j")

     }

  }

 

fun main(args: Array<String>) {
    var a = listOf(1, 2, 3, 4, 5)
    println(a)
    println(sum(3 , 4))
    foo()
    println("")
    foo2()
    println("")
    foo3()
    println("")
    foo4()
    println("")
    println(foo5())
}

fun sum(a: Int, b: Int):Int {
    return a + b
}

fun foo() {
    var ints = listOf(0, 1, 2, 3)
    ints.forEach(
            fun(value: Int){
                if(value == 1) return
                print(value)
            })
    print("End")
}

fun foo2() {
    var ints = listOf(0, 1, 2, 3)
    // 람다식에서 return 시 nearest enclosing 함수가 return 된다.
    ints.forEach {
                if(it == 1) return
                print(it)
            }
    print("End")
}

fun foo3() {
    var ints = listOf(0, 1, 2, 3)
    // 람다식에 대해서만 return 하려면 label 을 이용해야 한다.
    ints.forEach loop@{
                if(it == 1) return@loop
                print(it)
            }
    print("End")
}

fun foo4() {
    var ints = listOf(0, 1, 2, 3)
    // 람다식에 대해서만 return 하려면 label 을 이용해야 한다.
    // 암시적 레이블은 람다가 사용된 함수의 이름과 동일하다.
    ints.forEach {
        if(it == 1) return@forEach
        print(it)
    }
    print("End")
}

fun foo5(): List<String> {
    var ints = listOf(0, 1, 2, 3)
    val result = ints.map{
        if(it == 0){
            return@map "zero"
        }
        "No $it"
    }
    return result
}
 

 

 

'안드로이드 > Kotlin 문법' 카테고리의 다른 글

코틀린 접근 제한자  (0) 2020.01.14
코틀린 패키지  (0) 2020.01.13
코틀린 인터페이스  (0) 2020.01.13
코틀린 추상클래스  (0) 2020.01.12
코틀린(Kotlin) 클래스 선언 및 상속  (0) 2020.01.12
블로그 이미지

Link2Me

,
728x90

Firebase 에서 구글 인증으로 로그인 처리하는 방법에 대한 사항이다.


1. https://console.firebase.google.com/ 에 접속한다.

  

   프로젝트가 없으면 추가하고 있으면 선택한다.


2. 앱을 설정하는 일련의 과정은 그림으로 대신한다.


3. 앱 build.gradle 파일 내용

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"

    defaultConfig {
        applicationId "com.link2me.android.googleauth"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.firebase:firebase-analytics:17.2.0'
    implementation 'com.google.firebase:firebase-auth:19.1.0'  // Firebase 인증에 대한 항목을 추가
    implementation 'com.google.android.gms:play-services-auth:17.0.0'
}

apply plugin: 'com.google.gms.google-services'


4. XML Layout 작성

<RelativeLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center" >

    <!-- 구글 로그인 버튼 -->
    <com.google.android.gms.common.SignInButton
        android:id="@+id/sign_in_button"
        android:layout_width="254dp"
        android:layout_height="50sp">
    </com.google.android.gms.common.SignInButton>

    <ImageView
        android:id="@+id/sign_out_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/sign_in_button"
        android:background="@drawable/google_signout"/>

    <TextView
        android:id="@+id/mStatusTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/sign_in_button"
        android:layout_marginTop="99dp"
        android:text="TextView" />

</RelativeLayout >


5. 사용자 인증 코드 작성

https://firebase.google.com/docs/auth/android/google-signin?authuser=0 에서 필요한 코드를 복사하여 붙여넣기 한다.

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.GoogleAuthProvider;

public class GoogleSignInActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "GoogleActivity";
    private static final int RC_SIGN_IN = 9001;
    private FirebaseAuth mAuth;
    private GoogleSignInClient mGoogleSignInClient;

    private TextView mStatusTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_google);

        // Button listeners
        findViewById(R.id.sign_in_button).setOnClickListener(this);
        findViewById(R.id.sign_out_button).setOnClickListener(this);
        mStatusTextView = findViewById(R.id.mStatusTextView);

        // Configure Google Sign In
        GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestEmail()
                .build();

        mGoogleSignInClient = GoogleSignIn.getClient(this, gso);

        // Initialize Firebase Auth
        mAuth = FirebaseAuth.getInstance();
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.sign_in_button:
                signIn(); // 구글 로그인 버튼 클릭시
                break;
            case R.id.sign_out_button:
                signOut();
                break;
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        // Check if user is signed in (non-null) and update UI accordingly.
        FirebaseUser currentUser = mAuth.getCurrentUser();
        updateUI(currentUser);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
        if (requestCode == RC_SIGN_IN) {
            Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
            try {
                // Google Sign In was successful, authenticate with Firebase
                GoogleSignInAccount account = task.getResult(ApiException.class);
                firebaseAuthWithGoogle(account);
            } catch (ApiException e) {
                Log.w(TAG, "Google sign in failed", e);
                updateUI(null);
            }
        }
    }

    private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
        Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());

        AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
        mAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if (task.isSuccessful()) {
                            // Sign in success, update UI with the signed-in user's information
                            Log.e(TAG, "signInWithCredential:success");
                            FirebaseUser user = mAuth.getCurrentUser();
                            updateUI(user);
                            Log.e(TAG,"user getEmail : "+user.getEmail());
                            Log.e(TAG,"user getDisplayName : "+user.getDisplayName());
                            Log.e(TAG,"user getUid : "+user.getUid());
                        } else {
                            // If sign in fails, display a message to the user.
                            Log.w(TAG, "signInWithCredential:failure", task.getException());
                            Toast.makeText(GoogleSignInActivity.this, "Authentication Failed.", Toast.LENGTH_SHORT).show();
                            updateUI(null);
                        }

                        // [START_EXCLUDE]
                        //hideProgressDialog();
                        // [END_EXCLUDE]
                    }
                });
    }

    private void signIn() {
        Intent signInIntent = mGoogleSignInClient.getSignInIntent();
        startActivityForResult(signInIntent, RC_SIGN_IN);
    }

    private void signOut() {
        mAuth.signOut(); // Firebase sign out

        // Google sign out
        mGoogleSignInClient.signOut().addOnCompleteListener(this,
                new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        updateUI(null);
                    }
                });
    }

    private void revokeAccess() {
        // Firebase sign out
        mAuth.signOut();

        // Google revoke access
        mGoogleSignInClient.revokeAccess().addOnCompleteListener(this,
                new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        updateUI(null);
                    }
                });
    }

    private void updateUI(FirebaseUser user) {
        //hideProgressDialog();
        if (user != null) {
            mStatusTextView.setText(user.getEmail() + " : " + user.getDisplayName());

            findViewById(R.id.sign_in_button).setVisibility(View.GONE);
            findViewById(R.id.sign_out_button).setVisibility(View.VISIBLE);
        } else {
            mStatusTextView.setText("LogOut");

            findViewById(R.id.sign_in_button).setVisibility(View.VISIBLE);
            findViewById(R.id.sign_out_button).setVisibility(View.GONE);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //FirebaseAuth.getInstance().signOut();
    }
}


6. 로그인 후 Firebase 에 회원정보 저장 결과





블로그 이미지

Link2Me

,