728x90

XMPP Chat 기능 동작 테스트를 하려고 GitHub 자료를 다운로드 받아서 테스트 해보고 있다.

벌써 여러차례 로그인 실패를 겪으면서 GitHub 에 있는 자료와 동일한 환경으로 설정을 해보고자 프로젝트를 별도로 추가하는 방식으로 테스트하고 있다.


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:23.2.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:design:23.2.1'
    implementation 'com.android.support:recyclerview-v7:23.2.1'
    implementation 'org.igniterealtime.smack:smack-android:4.1.1'
    implementation 'org.igniterealtime.smack:smack-tcp:4.1.1'
    implementation 'org.igniterealtime.smack:smack-core:4.1.1'
    implementation 'org.igniterealtime.smack:smack-im:4.1.1'
    implementation 'org.igniterealtime.smack:smack-extensions:4.1.1'
    implementation 'org.igniterealtime.smack:smack-android-extensions:4.1.1'
    implementation 'org.igniterealtime.smack:smack-resolver-minidns:4.1.3'
    implementation 'org.igniterealtime.smack:smack-sasl-provided:4.1.1'
    implementation 'com.google.code.gson:gson:1.7.2'
    implementation 'com.google.android.gms:play-services:8.1.0'
}


<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".Login">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

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


AndroidManifest.xml 에서 에러가 발생하면 위와 같이 하면 해결되더라.


그런데 문제는
implementation 'com.google.android.gms:play-services:8.1.0' 때문에 multidex 에러가 발생하는지 오류가 또다시 발생한다.

이건 시도해보려고 했지만 성공하지 못했다.



블로그 이미지

Link2Me

,
728x90

2013년 Google I/O에서 발표한 volley라는 Http 라이브러리를 이용하여 로그인 예제를 만들어봤다.

구글 자료들을 받아서 테스트 하다보니 여러가지 라이브러리로 만들어져 있어서 내가 만든 앱에 적용을 해보면서 기능을 테스트하고 적어둔다.

Volley 라이브러리를 사용하면 코드가 심플해져 사용하기 편하다는 걸 알 수 있을 것이다.


build.gradle 추가

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:recyclerview-v7:27.1.1' // ListView 개선 버전
    implementation 'com.android.support:cardview-v7:27.1.1'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
}


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

https 통신을 기본으로 지원하고 http 통신은 기본 지원이 아니라서 아래 색깔 표시한 한줄을 추가해줘야 한다.

AndroidManifest.xml 파일에 추가할 사항

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

앱 build.gradle 은 androidX 로 변경한다. 구글링하면 나온다.

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'
}


Volley를 사용하는 방법은
1) RequestQueue를 생성한다.
2) Request Object를 생성한다.
3) 생성한 Object를 RequestQueue로 넘긴다.


서버에서 받은 데이터가 텍스트인 경우에는 StringRequest 을 사용하고, JSON 데이터일 경우에는 JsonObjectRequest 를 사용하면 된다.

서버에서 데이터를 텍스트로 만들어서 보내기 때문에 StringRequest 를 사용하면 된다. 이렇게 하는 이유는 서버에서 에러가 발생했을 때 텍스트로 보내면 에러의 원인을 발견하고 서버 코드를 수정하기 쉽기 때문이다.


private void LoginVolley(final String loginID, final String loginPW) {
    // 1. RequestQueue 생성 및 초기화
    RequestQueue requestQueue = Volley.newRequestQueue(context);

    String url = Value.IPADDRESS + "/loginChk.php";

    // 2. Request Obejct인 StringRequest 생성
    StringRequest request = new StringRequest(Request.Method.POST, url,
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    Log.d("result", "[" + response + "]");
                }
            },
            new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d("error", "[" + error.getMessage() + "]");
                }
            }) {
        @Override
        protected Map<String, String> getParams() throws AuthFailureError {
            Map<String, String> params = new HashMap<>();
            params.put("loginID", Value.encrypt(loginID));
            params.put("loginPW", Value.encrypt(loginPW));
            return params;
        }
    };

    // 3) 생성한 StringRequest를 RequestQueue에 추가
    requestQueue.add(request);
}



실제 사용하는 코드를 보면서 이해도를 높여보자.

코딩하면서 항상 신경써서 고려할 사항은 Log 를 보면서 문제가 서버쪽에 있는 것인지 안드로이드 코드에 문제가 있는 것인지 파악할 수 있어야 한다.


 private void LoginVolley(final String loginID, final String loginPW) {
    // 1. RequestQueue 생성 및 초기화
    RequestQueue requestQueue = Volley.newRequestQueue(context);

    String url = Value.IPADDRESS + "/loginChk.php";

    // 2. Request Obejct인 StringRequest 생성
    StringRequest request = new StringRequest(Request.Method.POST, url,
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    Log.d("result", "[" + response + "]"); // 서버와의 통신 결과 확인 목적
                    showJSONList(response);
                }
            },
            new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d("error", "[" + error.getMessage() + "]");
                }
            }) {
        @Override
        protected Map<String, String> getParams() throws AuthFailureError {
            Map<String, String> params = new HashMap<>();
            params.put("loginID", Value.encrypt(loginID));
            params.put("loginPW", Value.encrypt(loginPW));
            params.put("uID", Value.encrypt(Value.getPhoneUID(context)));
            params.put("AppVersion", Value.VERSION);
            params.put("phoneVersion", Build.VERSION.RELEASE);
            params.put("phoneBrand", Build.BRAND);
            params.put("phoneModel", Build.MODEL);
            params.put("keyword", Value.encrypt(Value.URLkey()));
            return params;
        }
    };

    // 3) 생성한 StringRequest를 RequestQueue에 추가
    requestQueue.add(request);
}

private void showJSONList(String JSONdata) {
    // 서버 정보를 파싱하기 위한 변수 선언
    try {
        JSONObject jsonObj = new JSONObject(JSONdata);
        JSONArray logininfo = jsonObj.getJSONArray("result");

        JSONObject c = logininfo.getJSONObject(0);
        idx = Value.decrypt(c.getString("IDX"));
        dept1Code = Value.decrypt(c.getString("Dept1Code"));
        dept2Code = Value.decrypt(c.getString("Dept2Code"));

        System.out.println("idx : " + idx);

        if (Integer.parseInt(idx) > 0) { // 로그인 정보 일치
            pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = pref.edit();

            editor.putString("id", loginID);
            editor.putString("pw", loginPW);
            editor.putString("autologin", "checked"); // 로그인되면 무조건 자동로그인 처리
            editor.putString("idx", idx); // 로그인 사용자의 idx
            editor.putString("category1", dept1Code);
            editor.putString("category2", dept2Code);
            editor.putString("uID", uID);
            editor.putString("registrationID", null);

            editor.commit();

            startActivity(new Intent(getApplication(), Main.class));
            finish(); // 현재 Activity 를 없애줌

        } else if (idx.equalsIgnoreCase("-1")) { // 등록된 단말기와 불일치
            deviceDismatch_showAlert();
        } else if (idx.equalsIgnoreCase("0")) {
            showAlert();
        } else if (idx.equalsIgnoreCase("-2")) {
            showDisAgree();
        } else if (idx.equalsIgnoreCase("-3")) {
            showDeny();
        } else {
            Toast.makeText(Login.this, "서버로부터 정보가 잘못 전송되었습니다", Toast.LENGTH_SHORT).show();
        }

    } catch (JSONException e) {
        e.printStackTrace();
    }
}


참고 사항

ㅇ Android 로그인 폼 예제(https://link2me.tistory.com/1532)  에 이 코드를 추가해 보면 편하다.

ㅇ Anroid JSON 파헤치기 https://link2me.tistory.com/1247


이 글이 도움되었다면 000 부탁드립니다.

블로그 이미지

Link2Me

,
728x90

새로운 기능을 계속 테스트하다보니 로그인 화면 만드는 것을 자주 시도해야 해서 너무 귀찮아서 가장 기본적인 폼을 만드는 1단계 코드만 작성해서 오픈한다. 저같은 초보자, 입문자에게는 도움이 될 것으로 본다.


테스트 환경

Android Studio 3.2, Android 8.0


이 코드는 서버와 통신하는 기능을 배제한 코드다.


LoginForm.zip



dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:recyclerview-v7:27.1.1' // ListView 개선 버전
    implementation 'com.android.support:cardview-v7:27.1.1'
    implementation 'gun0912.ted:tedpermission:2.0.0'
}


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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".Login">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>


 package com.dcdroid.chatapp;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.util.ArrayList;

public class Login extends AppCompatActivity {
    private static String TAG = "LoginActivity";
    Context context;
    public SharedPreferences pref;
    EditText etId;
    EditText etPw;
    String loginID;
    String loginPW;
    CheckBox autologin;

    public static int OVERLAY_PERMISSION_REQ_CODE = 1234;

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

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

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

        // 네트워크 연결상태 체크
        if (NetworkConnection() == false) NotConnected_showAlert();
        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.CALL_PHONE,  // 전화걸기 및 관리
                            android.Manifest.permission.WRITE_CONTACTS, // 주소록 액세스 권한
                            android.Manifest.permission.READ_EXTERNAL_STORAGE,
                            android.Manifest.permission.WRITE_EXTERNAL_STORAGE // 기기, 사진, 미디어, 파일 엑세스 권한
                            //android.Manifest.permission.RECEIVE_SMS //, // 문자 수신
                            //android.Manifest.permission.CAMERA
                    })
                    .check();

        } else {
            initView();
        }
    }

    public void checkPermissionOverlay() {
        if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
            Intent intentSettings = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intentSettings, OVERLAY_PERMISSION_REQ_CODE);
        }
    }

    @TargetApi(Build.VERSION_CODES.M)
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
            if (!Settings.canDrawOverlays(this)) {
                // You don't have permission
            } else {
                Toast.makeText(this, "오버레이 권한 없이는 발신자정보 팝업을 이용할 수 없습니다...", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void initView() {
        checkPermissionOverlay(); // 오버레이 권한 설정

        findViewById(R.id.setting).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (Build.VERSION.SDK_INT >= 26) {
                    NonSecretApp_Setting(); // 출처를 알 수 없는 앱 설정 화면 띄우기
                } else {
                    startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
                }
            }
        });

        etId = (EditText) findViewById(R.id.login_id_edit);
        etPw = (EditText) findViewById(R.id.login_pw_edit);
        autologin = (CheckBox) findViewById(R.id.autologinchk);

        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        String savedID = pref.getString("id", "");
        etId.setText(savedID);
        String passwd = pref.getString("pw", ""); // 보안상 여기에 적으면 안되는 코드지만 테스트 목적이라...
        etPw.setText(passwd);
        if (!pref.getString("id", "").equals(""))
            etPw.requestFocus();
        String chk1 = pref.getString("autologin", "");
        if (chk1.equals("checked"))
            autologin.setChecked(true);

        if (!pref.getString("loginID", "").equals("")) etPw.requestFocus();

        Button submit = (Button) findViewById(R.id.login_btn);
        submit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loginID = etId.getText().toString().trim();
                loginPW = etPw.getText().toString().trim();
                if (loginID != null && !loginID.isEmpty() && loginPW != null && !loginPW.isEmpty()) {
                    // 여기에 적을 코드는 아닌데 테스트 목적이다보니 귀차니즘으로 여기에 적음
                    pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
                    SharedPreferences.Editor editor = pref.edit();

                    editor.putString("id", loginID);
                    editor.putString("pw", loginPW);
                    editor.putString("autologin", "checked"); // 로그인되면 무조건 자동로그인 처리
                    editor.commit();
                    // 실제 로그인할 함수 실행 (아이디와 패스워드 정보를 넘겨줌)
                    // 블로그에 오픈된 코드를 찾아서 같이 활용하면 서버와의 통신이 가능
                    new UserLoginTask().execute(loginID,loginPW);
                }
            }
        });
    }

    private void NonSecretApp_Setting() {
        if (Build.VERSION.SDK_INT >= 26) { // 출처를 알 수 없는 앱 설정 화면 띄우기
            PackageManager packageManager = getPackageManager();
            boolean isTrue = packageManager.canRequestPackageInstalls();

            if (!packageManager.canRequestPackageInstalls()) {
                AlertDialog.Builder b = new AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Light_Dialog);
                b.setTitle("알림");
                b.setMessage("보안을 위해 스마트폰 환경설정의 '앱 설치 허용'을 설정해 주시기 바랍니다.설정화면으로 이동하시겠습니까?");
                b.setCancelable(false);
                b.setPositiveButton("설정하기", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
                        intent.setData(Uri.parse("package:" + getPackageName()));
                        startActivity(intent);
                    }
                });

                b.setNegativeButton("건너띄기", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                });
                b.show();
            }
        }
    }

    private void NotConnected_showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("네트워크 연결 오류");
        builder.setMessage("사용 가능한 무선네트워크가 없습니다.\n" + "먼저 무선네트워크 연결상태를 확인해 주세요.")
                .setCancelable(false)
                .setPositiveButton("확인", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        finish(); // exit
                        //application 프로세스를 강제 종료
                        android.os.Process.killProcess(android.os.Process.myPid());
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();

    }

    private boolean NetworkConnection() {
        int[] networkTypes = {ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_WIFI};
        try {
            ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
            for (int networkType : networkTypes) {
                NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
                if (activeNetwork != null && activeNetwork.getType() == networkType) {
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    private class UserLoginTask  extends AsyncTask<String, Void, Boolean> {
        // String 으로 값을 전달받은 값을 처리하고, Boolean 으로 doInBackground 결과를 넘겨준다.
        @Override
        protected Boolean doInBackground(String... strings) {
            return null;
        }
    }
}


나머지는 첨부파일 받아서 이용하면 된다.


AsyncTask 함수는 http://link2me.tistory.com/1516 참조.

AsyncTask 대신에 Volley 라이브러리 사용하는 법은 http://link2me.tistory.com/1533 참조



도움이 되셨다면 000 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

블로그 이미지

Link2Me

,
728x90

https://tinypng.com/ 에 이미지 파일을 Drag&Drop 하면 이미지 해상도가 낮춰져 이미지 용량이 줄어든다.




포토삽 기본 사용법

CTRL - : 화면 축소
CTRL + : 화면 확대
오른쪽 마우스 누르면 이름 설명이 나오고 다른 툴로도 변경 가능
네이버에서 나눔고딕 글꼴을 다운로드하여 더블 클릭하여 설치를 한다.

포토샵 PSD 파일을 더블 클릭하여 실행한다.

블로그 이미지

Link2Me

,
728x90

Node.js 채팅 기본코드를 좀 더 수정 보완하고 Web 채팅과 안드로이드 채팅이 동시에 되는 걸 확인했다.

Node.js 설치 및 Express, socket.io 설치에 대한 사항은 이전 게시글을 참조하면 된다.




서버 chat.js


 const express = require('express'),
    http = require('http'),
    app = express(),
    server = http.createServer(app),
    io = require('socket.io').listen(server);

app.get('/', (req, res) => {
    res.sendFile(__dirname+'/index.html');
});

var numUsers=0;
var usernames=[];
io.on('connection', (socket) => {
    var addedUser = false;
    socket.on('join', function (username) {
        if (addedUser) return;
        socket.username = username;
        ++numUsers;
        addedUser = true;
        usernames.push(socket.username);
        console.log(socket.username + "님이 접속하셨습니다.");
        console.log('접속한 사용자수 : '+numUsers+'명');
        socket.broadcast.emit('userjoinedthechat', socket.username + "님이 접속하셨습니다.");
        updateUsernames();
    });

    function updateUsernames(){
        io.sockets.emit('usernames', usernames);
    }

    socket.on('messagedetection', (senderNickname, messageContent) => {
        // 서버 콘솔상에 메시지 보여주기
        console.log(senderNickname + " :" + messageContent);
        //message object 생성
        let message = {"message": messageContent, "senderNickname": senderNickname}
        // 클라이언트(Web, Android)로 메시지 전송
        io.emit('message', message); // 나를 포함한 모든 클라이언트에게 전송
    });

    socket.on('disconnect', function () {
        if (addedUser) {
            --numUsers;
            usernames.splice(usernames.indexOf(socket.username), 1);
        }
        console.log(socket.username+"님이 퇴장했습니다");
        console.log('접속한 사용자수 : '+numUsers+'명');
        socket.broadcast.emit("userdisconnect", socket.username + "님이 퇴장했습니다");
        // 나를 제외한 모든 클라이언트에게 전송
        updateUsernames();
    });

});

server.listen(3000, () => {
    console.log('Node chat app is running on port 3000');
});


Web Chatting UI

- css 파일을 별도 분리하여 인식시켰더니 감지를 못하더라. 그래서 같은 파일에 추가했다.

- 채팅 메시지가 많으면 자동으로 가장 하단의 메시지 내용이 보이도록 처리

- 줄바꿈 처리 기능 코드 추가 (스마트폰에서는 엔터키 입력 안되게 처리 코드 추가)

- Web 채팅과 Android 채팅 동시에 가능

- 반응형 화면으로 스마트폰과 PC에서 보이는 화면이 다르게 처리 (커스터마이징 필요)


<!DOCTYPE html>
<html>
<head>
<title>Web Chat Messenger</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" type="text/css" rel="stylesheet">
<style type="text/css">
.container{max-width:1170px; margin:auto;}
img{ max-width:100%;}

.messaging { padding: 0 0 50px 0;}

.inbox_msg {
  border: 1px solid #c4c4c4;
  clear: both;
  overflow: hidden;
}
.inbox_people {
  background: #f8f8f8 none repeat scroll 0 0;
  float: left;
  overflow: hidden;
  width: 35%; border-right:1px solid #c4c4c4;
}
.top_spac{ margin: 20px 0 0;}

.headind_srch{ padding:10px 29px 10px 20px; overflow:hidden; border-bottom:1px solid #c4c4c4;}
.recent_heading {float: left; width:100%;}
.recent_heading h4 {
  color: #05728f;
  font-size: 21px;
  margin: auto;
}

.inbox_chat { height: 550px; overflow-y: scroll;}

.mesgs {
  float: left;
  padding: 20px 5px 5px 10px;
  width: 65%;
}
.msg_history {
  height: 500px;
  overflow-y: auto;
  background: #FFFFFF;
}

.incoming_msg_img {
  display: inline-block;
  width: 6%;
}
.received_msg {
  display: inline-block;
  padding: 0 0 0 10px;
  vertical-align: top;
  width: 92%;
 }
.received_withd_msg { width: 77%;}
.received_withd_msg p {
  background: #F3E2A9 none repeat scroll 0 0;
  border-radius: 3px;
  color: #646464;
  font-size: 14px;
  margin: 0;
  padding: 5px 10px 5px 12px;
  width: 100%;
}
.time_date {
  color: #747474;
  display: block;
  font-size: 12px;
  margin: 8px 0 0;
}

.type_msg {border-top: 1px solid #c4c4c4;position: relative;}
.sent_msg {
  float: right;
  width: 76%;
}
.sent_msg p {
  background: #05728f none repeat scroll 0 0;
  border-radius: 3px;
  font-size: 14px;
  margin: 0; color:#fff;
  padding: 5px 10px 5px 12px;
  width:100%;
}
.outgoing_msg{ overflow:hidden; margin:26px 0 26px;}
.input_msg_write input {
  background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
  border: medium none;
  color: #4c4c4c;
  font-size: 15px;
  min-height: 48px;
  width: 100%;
}

.msg_send_btn {
  background: #05728f none repeat scroll 0 0;
  border: medium none;
  border-radius: 50%;
  color: #fff;
  cursor: pointer;
  font-size: 17px;
  height: 33px;
  position: absolute;
  right: 0;
  top: 11px;
  width: 33px;
}


@media screen and (max-width: 800px) {
    .inbox_msg {
      display: block;
      width: 100%;
    }
    .inbox_people {width: 100%;}
    .inbox_chat { width: 100%;height: 250px;}

    .mesgs {
      display: block;
      width: 100%;
      padding: 20px 0px 10px 0px;
    }
    .msg_history {width: 100%;height: 450px;}
    .type_msg {width: 100%;}

}

</style>
</head>

<body>
<div class="container">
    <!--Form to enter UserName-->
    <div class="row" id="nickWrap">
        <div class="col-xs-9">
            <form id="setNick">
                <input id="username" class="form-control" placeholder="사용자명을 입력하세요"/>
                <input type="submit" class="btn btn-primary">
            </form>
        </div>
    </div>
    <!--end username-->
<div class="messaging" id="contentWrap">
<h3 class=" text-center" id="msg_leave">Messaging</h3>
    <div class="inbox_msg">
        <div class="inbox_people">
          <div class="headind_srch">
            <div class="recent_heading">
              <h4>접속 사용자 현황</h4>
            </div>
          </div>

        <div class="inbox_chat">
            <div class="panel panel-default">
                <div class="panel-body" id="users"></div>
            </div>           
        </div>
    </div>

    <div class="mesgs">
        <div class="msg_history" id="chat">
        </div>

        <div class="type_msg">
            <form id="send-message">
            <div class="input_msg_write">
              <textarea rows="3" id="message" class="form-control" placeholder="메시지를 입력하세요" /></textarea>
              <button class="msg_send_btn" type="button"><i class="fa fa-paper-plane-o" aria-hidden="true"></i></button>
            </div>
            </form>
        </div>
    </div>
</div>

<!--node.js and Jquery code-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
<script>
$(function () {
    var socket = io();
    var myname;
    var i;
    var filter = "win16|win32|win64|mac|macintel";

    $('#contentWrap').hide();

    $('#setNick').submit(function () {
        myname = $('#username').val();
        if(myname.length == 0) {
            alert('이름을 입력하세요');
            return false;
        }
        socket.emit('join', myname);
        $('#nickWrap').hide();
        $('#contentWrap').show();
        return false;
    });

    //List of users
    socket.on('usernames', function (message) {
        var html = '';
        for (i = 0; i < message.length; i++) {
            html += message[i] + '<br/>'
        }
        $('#users').html(html);
    });

    $('.msg_send_btn').click(function (e) {
        e.preventDefault();
        msg_send();
    });

    $('#message').keypress(function (e) {
        if ( navigator.platform ) { // 모바일 접속이면 엔터키를 동작 안되게 처리
            if(filter.indexOf( navigator.platform.toLowerCase() ) > 0){
                if (e.which == 13 && !e.shiftKey) { // Shift + Enter키이면 줄바꿈처리 가능
                    msg_send();
                    return false;
                }
            }
        }
    });

    function msg_send(){
        if($('#message').val().length ==0){
            alert('메시지가 입력되지 않았습니다');
            $('#message').focus();
            return false;
        }
        socket.emit('messagedetection', myname, $('#message').val());
        $('#message').val(''); // 메시지를 전송하고 메시지 전송 창 내용을 비운다.
    }

    socket.on('message', function (msg) {
        msg.message = msg.message.replace(/(?:\r\n|\r|\n)/g, '<br />'); // 줄바꿈 처리
        if(myname == msg.senderNickname){ // 내가 전송한 메시지
            $('#chat').append('<div class="outgoing_msg"><div class="sent_msg"><p> '+ msg.message +'</p><span class="time_date">'+new Date().toLocaleString()+'</span></div></div>');
        } else { // 상대방이 보낸 메시지
            $('#chat').append('<div class="incoming_msg"><div class="incoming_msg_img"> <img src="https://ptetutorials.com/images/user-profile.png" alt="sunil"> </div><div class="received_msg"><div class="received_withd_msg"><p>'+ msg.message +'</p><span class="time_date">'+msg.senderNickname+', '+new Date().toLocaleString()+'</span></div></div></div>');
        }
        $("#chat").scrollTop($("#chat")[0].scrollHeight); // 최근 메시지 출력된 내용이 보이게 처리
    });

    socket.on('userjoinedthechat', function (msg) {
        $('#chat').append('<div class="incoming_msg"><div class="incoming_msg_img"></div><div class="received_msg"><div class="received_withd_msg"><p>'+ msg +'</p><span class="time_date">'+new Date().toLocaleString()+'</span></div></div></div>');
        $("#chat").scrollTop($("#chat")[0].scrollHeight); // 최근 메시지 출력된 내용이 보이게 처리
    });
   
    socket.on('userdisconnect', function (msg) {
        $('#chat').append('<div class="incoming_msg"><div class="incoming_msg_img"></div><div class="received_msg"><div class="received_withd_msg"><p>'+ msg +'</p><span class="time_date">'+new Date().toLocaleString()+'</span></div></div></div>');
        $("#chat").scrollTop($("#chat")[0].scrollHeight); // 최근 메시지 출력된 내용이 보이게 처리
    });

});
</script>
</body>
</html>



채팅 UI는 bootstrap 코드를 검색해서 원하는 코드로 변경하면 모바일 Web에서도 깔끔하게 보이게 처리할 수 있을 것이다.

블로그 이미지

Link2Me

,
728x90

Singleton Design Pattern is basically used when we want to limit object creation.
최초 한번만 메모리를 할당하고(static) 그 메모리를 인스턴스로 만들어 사용하는 디자인 패턴.
싱글톤(singleton)은 단독 객체라는 의미를 가진 영단어로, 단 하나의 객체만 만들어야 할 경우 필요하다.
싱글톤 패턴은 하나의 인스턴스만을 재사용하게 된다. 따라서 객체를 여러 번 생성할 필요가 없는 경우에 싱글톤을 사용하면 불필요한 자원 낭비나 오버헤드 등을 막을 수 있다.


class 클래스  {

    // 정적 필드

    // 자신의 클래스 타입으로 정적(static) 필드를 선언하고 자신의 인스턴스를 생성하여 초기화를 한다.

    // 전역 변수로 instance를 만드는데 private static을 이용한다.

   // static 는 이 객체를 사용하는 모두에게 이 값을 동일시 하겠다는 의미다.

    private static 클래스 instance = new 클래스(); // ② class 외부에서 값을 변경할 수 없도록 private 를 붙인다.


    //생성자   

    private 클래스 (){  // ① 외부에서 new 연산자로 생성자를 호출할 수 없도록 private 를 붙인다.
       
    }


    // 정적 메소드

    public static 클래스 getInstance(){ // ③ 외부에서 호출이 가능한 정적 메소드인 getInstance()를 정의한다.
        return instance; // ④ 정적 필드에서 참조하고 있는 자신의 객체를 리턴하도록 한다.
    }
   

}


- 여기서 클래스명 = Singleton 으로 가정한다.

- 생성자의 접근 제어자를 private로 하면, Class 외부에서 new 키워드를 사용할 수 없게 된다.

  즉, Singleton 클래스를 외부에서 호출할 수 없다.

  Singleton singeton = new Singleton(); // 에러 (외부에서 생성자에 접근할 수 없으므로 인스턴스 생성불가)

  클래스 내부에서는 인스턴스의 생성이 가능하다.

- 생성자의 접근 제어자를 public로 하면, Class 외부에서 new 키워드를 사용할 수 있게 된다.
  Singleton singleton1 = new Singleton();
  Singleton singleton2 = new Singleton();
  서로 다른 new 연산자로 인해서 객체가 생성이 되었기 때문에 서로 다른 메모리를 할당받게 된다.

- 외부에서 호출이 가능한 정적 메소드인 getInstance()를 정의한다.
  이때 정적 필드에서 생성한 인스턴스를 리턴값으로 돌려준다.
  Singleton singleton1 = Singleton.getInstance(); // 싱글톤 인스턴스 호출
  Singleton singleton2 = Singleton.getInstance(); // 싱글톤 인스턴스 호출
  if(singleton1 == singleton2){
      System.out.println("두 객체는 같습니다.");
  } else {
      System.out.println("두 객체는 다릅니다.");
  }

- 안드로이드 앱은 각 액티비티나 클래스 별로 주요 클래스들을 일일이 전달하기가 번거롭기 때문에 싱글톤 클래스를 만들어서 접근하도록 설계하는 것이 편하다.
  하지만 주의하자. stakoverflow 게시글 https://stackoverflow.com/questions/26882594/static-singleton-class-memory-leak-in-android 을 보면 99.9%는 안전하지만, If you call getInstance() from two different threads can happen that you instantiate Singleton twice.


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

자바 Arraylist  (0) 2019.05.26
Java BubbleSort  (0) 2019.05.13
[Java] instanceof 연산자  (0) 2017.10.24
Java static 변수 이해하기  (0) 2017.10.20
[Java] 정보은닉과 캡슐화  (0) 2017.10.16
블로그 이미지

Link2Me

,
728x90

XML에 기반한 실시간 메시지 지향 공개 표준 통신프로토콜인 XMPP(Extensible Messaging and Presence Protocol)을 사용한 오픈소스 서버와 클라이언트가 이미 공개돼 있다.

이를 이용해서 간단히 메신저를 구축 할 수 있다.


유투브 동영상 강좌를 우연히 보다가 openfire 메신저를 설치해봤다.


설치환경 : CentOS 6.6 64bit, MySQL 5.6.30


설치하면서 MySQL 구동중인 것이 장애가 발생하는 증상이 있어서 MySQL 문제발생을 조치하고 나서, MySQL을 stop시킨 후 openfire 를 Stop ==> Remove 한 다음 재설치를 하고 나서 성공했다.


OpenFire 사이트 : https://www.igniterealtime.org/downloads/index.jsp


/etc/init.d/mysqld start
/etc/init.d/mysqld stop

0. 가장 먼저 해야 할 일은 MySQL DB를 stop 시키고 나서 설치를 하기 바란다.
    에러가 나면 에러 원인 찾아서 해결하기가 쉽지 않더라.

1. mkdir -p /home/openfire

2. openfire.rpm 다운받기
   wget http://igniterealtime.org/openfire/openfire-4.2.3-1.x86_64.rpm  

3. rpm 설치하기
   yum install openfire-4.2.3-1.x86_64.rpm
   설치를 하면 /opt/openfire 디렉토리에 설치된다.
   설치했던 openfire 를 삭제하는 방법은 yum remove openfire 다. MySQL DB를 stop하고 하는 것이 좋을 거 같다.

4. service 시작하기
   service openfire start

5. 방화벽 설정에 9090 포트를 추가한다.


6. MySQL DB에서 openfire 라는 DB를 생성한다.

    /etc/init.d/mysqld start

    MySQL 접속 후 openfire DB 생성


7. Web 브라우저 콘솔 접속
   http://ipaddress:9090

   순서에 따라 설치
   DB Setting 화면에서 설정을 잘못하면 고생을 할 수 있다.


Database URL : jdbc:mysql://localhost:3306/openfire

Username : root 또는 별도 생성한 db_username

Password : root 패스워드 또는 db_username의 패스워드


이 부분을 신경쓰면 설치하는데는 문제없이 잘 넘어간다.

admin 과 지정한 패스워드를 입력하면 관리자 화면으로 접속된다.



phpMyAdmin 으로 살펴보면

openfire DB에 테이블이 34개 생성되어 있는 걸 알 수 있다.




이제 http://www.igniterealtime.org/downloads/index.jsp 에서 Spark 를 받아서 설치한다.





시스템이 재부팅되어도 openfire 자동 실행되도록 설정
#chkconfig --add openfire
#chkconfig openfire on


Android 어플 개발시에 OpenFire 서버와 통신하기 위해서 Security : SSL Certificates 가 필요하다고 한다.
Because our server doesn’t have a public facing domain name, and is running locally, we can’t set up an SSL certificate for it. So we will need to disable security when connecting to our server as shown below
.setDebuggerEnabled(true)
.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled) // Should make required on Public Server with SSL Configured
.setCompressionEnabled(true).build();


검색하다보니 XMPP 또다른 서버가 있다.




블로그 이미지

Link2Me

,
728x90

Android EditText에서 줄 바꿈을 추가하는 방법에 대해 검색해보니 여러가지 방법이 나오는데

 

<EditText
    android:id="@+id/message"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_weight="3"
    android:inputType="textMultiLine"
    android:hint="your message" />

 

이 한줄만 추가해주고 입력해보니 줄바꿈되면서 입력이 잘 된다.

 

채팅 기능 구현해보려 하다보니 EditText 기능에 대한 세밀한 공부를 하게 된다.

 

<EditText
    android:inputType="textMultiLine" <!-- inputType에 MultiLine을 설정 -->
    android:lines="8" <!-- 화면에 8줄로 보이게한다. -->
    android:minLines="6" <!-- 최소 6줄 -->
    android:maxLines="10" <!-- 최대 10줄 -->
    android:layout_height="wrap_content"
    android:layout_width="fill_parent"
    android:scrollbars="vertical" <!-- 수직 스크롤 바  설정 -->
/>

 

 

관련 참조 게시글

https://code.i-harness.com/ko-kr/q/40999a

 

 

블로그 이미지

Link2Me

,
728x90

Android RecyclerView TextView 에 보여줄 내용에 자동으로 Link를 거는 방법을 찾아봤다.


간단하게

<TextView
    android:id="@+id/tv_chatMsg"
    android:text="Hello"
    android:textSize="14dp"
    android:autoLink="phone|web|email|map"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentEnd="true"
    android:layout_alignParentTop="true"
    android:background="@drawable/chat_from"
    />


한줄을 추가해주면 되더라.


하이퍼링크 색상은 지정하지 않으면 빨간색인데 별도 색으로 지정하고 싶으면 android:textColorLink="@color/blue" 를 해주면 된다.


텍스트를 클립보드로 복사하는 기능을 추가하고 싶다면

<TextView
    android:id="@+id/tv_chatMsg"
    android:text="Hello"
    android:textSize="14dp"
    android:textColorLink="@color/blue"
    android:textIsSelectable="true"
    android:autoLink="all"
    android:layout_marginTop="3dp"
    android:layout_below="@id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />

이 한줄을 추가해 주면 된다. 이 속성은 API Level 11부터 지원된다.

자바 코드에서는 msg.setTextIsSelectable(true); 로 설정해주면 된다.
xml 에서 android:textIsSelectable="true" 를 추가하거나 자바 코드에서 추가하거나 둘 중 하나면 하면 된다.


특정 단어를 눌렀을 때 URL 이 동작되도록 하고 싶은 경우에는 이 방법으로는 해결이 안된다.

특정 단어를 눌렀을 때는 http://gun0912.tistory.com/66 박상권님 블로그 참조.


전화번호를 눌렀을 때 바로 전화를 거는 게 아니라 문자를 보낼 수도 있고 복사를 하고 싶은 경우도 있다면....

https://blog.uncommon.is/a-better-way-to-handle-links-in-textview-27bb70b2d31c 참조


msg = (TextView) itemView.findViewById(R.id.tv_chatMsg);
msg.setMovementMethod(BetterLinkMovementMethod.getInstance());
Linkify.addLinks(msg,Linkify.ALL);

로 해봤지만 단순 autoLink 와 변화된 것은 없다.

블로그 이미지

Link2Me

,
728x90

Android 채팅창 구현을 해보려고 시도하면서 SoftKeyboard 가 활성화되면 Recyclerview 입력한 내용이 자동으로 스크롤 되는 기능이 제공되어야 할 거 같아서 EditText 터치이벤트 등으로 시도를 해보왔으나 실패!!!


http://givenjazz.tistory.com/54 에 나온 코드가 내가 원하는 코드였다.

이 코드 덕분에 코드가 좀 더 정교해지고 있는거 같다.


import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

public class SoftKeyboardDectectorView extends View {
    // 출처 : http://givenjazz.tistory.com/54
    private boolean mShownKeyboard;
    private OnShownKeyboardListener mOnShownSoftKeyboard;
    private OnHiddenKeyboardListener onHiddenSoftKeyboard;

    public SoftKeyboardDectectorView(Context context) {
        this(context, null);
    }

    public SoftKeyboardDectectorView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Activity activity = (Activity)getContext();
        Rect rect = new Rect();
        activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        int statusBarHeight = rect.top;
        int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();
        int diffHeight = (screenHeight - statusBarHeight) - h;
        if (diffHeight > 100 && !mShownKeyboard) { // 모든 키보드는 100px보다 크다고 가정
            mShownKeyboard = true;
            onShownSoftKeyboard();
        } else if (diffHeight < 100 && mShownKeyboard) {
            mShownKeyboard = false;
            onHiddenSoftKeyboard();
        }
        super.onSizeChanged(w, h, oldw, oldh);
    }

    public void onHiddenSoftKeyboard() {
        if (onHiddenSoftKeyboard != null)
            onHiddenSoftKeyboard.onHiddenSoftKeyboard();
    }

    public void onShownSoftKeyboard() {
        if (mOnShownSoftKeyboard != null)
            mOnShownSoftKeyboard.onShowSoftKeyboard();
    }

    public void setOnShownKeyboard(OnShownKeyboardListener listener) {
        mOnShownSoftKeyboard = listener;
    }

    public void setOnHiddenKeyboard(OnHiddenKeyboardListener listener) {
        onHiddenSoftKeyboard = listener;
    }

    public interface OnShownKeyboardListener {
        public void onShowSoftKeyboard();
    }

    public interface OnHiddenKeyboardListener {
        public void onHiddenSoftKeyboard();
    }
}
 


사용법

public class ChatBoxActivity extends AppCompatActivity {
    Context context;

    public RecyclerView listView;
    public ArrayList<Chat_Item> chatItemList = new ArrayList<>(); ;
    RecyclerView.Adapter listViewAdapter;
    RecyclerView.LayoutManager layoutManager;

    public  EditText editText;
    public  Button send ;
    //declare socket object
    private Socket socket;
    public String Nickname;


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

        editText = (EditText) findViewById(R.id.message) ;
        send = (Button)findViewById(R.id.send);
        // get the nickame of the user
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        String userNM = pref.getString("userNM", "");
        String userID = pref.getString("userID", "");
        Nickname = userNM +"("+userID+")";

        final SoftKeyboardDectectorView softKeyboardDecector = new SoftKeyboardDectectorView(this);
        addContentView(softKeyboardDecector, new FrameLayout.LayoutParams(-1, -1));

        softKeyboardDecector.setOnShownKeyboard(new SoftKeyboardDectectorView.OnShownKeyboardListener() {
            @Override
            public void onShowSoftKeyboard() {
                //키보드 등장할 때 채팅창 마지막 입력 내용을 바로 보이도록 처리
                listView.scrollToPosition(chatItemList.size()-1);
            }
        });

        try {
            socket = IO.socket(Value.ChatLive); // 채팅서버 주소
            socket.connect();
            socket.emit("join", Nickname);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }


       // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        listView = (RecyclerView) findViewById(R.id.messagelist);
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
        listView.setLayoutManager(layoutManager);
        listView.setItemAnimator(new DefaultItemAnimator());

        listViewAdapter = new ListViewAdapter(chatItemList, this); // Adapter 생성
        listView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅

    }
}
 


채팅 내용을 마지막으로 입력한 내용이 소트프키보드 입력창 위에 바로 보이도록 하는 코드다.

블로그 이미지

Link2Me

,
728x90
Android 채팅 라이브러리를 추가하면 에러가 발생한다.

implementation('com.github.nkzawa:socket.io-client:0.5.0') {
    exclude group: 'org.json', module: 'json'
}

원인을 알고보니 targetSdkVersion 26 에서는 com.github.nkzawa:socket.io-client:0.3.0 으로 하면 정상 동작한다.
com.github.nkzawa:socket.io-client:0.5.0 은 targetSdkVersion 27 에서 정상 동작하더라.

Creating a realtime chat app with android , NodeJs and Socket.io 에 대한 최신 예제는 https://dev.to/medaymentn/creating-a-realtime-chat-app-with-android--nodejs-and-socketio-4o55 를 참조하여 실행하면 Node.js 와 안드로이드간 통신이 간단하게 된다는 걸 확인할 수 있다.

회원정보와 연동하고, 내가 보낸 메시지, 상대방이 보낸 메시지를 구분하는 것은 안된다.

회원정보 연동은 Android-PHP-MySQL 통신으로 획득한 정보를 Node.js-Android 간에 활용하면 된다.
내가 보낸 메시지와 상대방이 보낸 메시지 구분은 RecyclerView 기능을 활용하면 해결 할 수 있다.


블로그 이미지

Link2Me

,
728x90

Node.js 와 MySQL 을 연동 테스트하려고 설치를 하는 도중에 mysql 에러가 발생한 거 같다.


Not connected :Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)


ERROR! The server quit without updating PID file (/usr/local/mysql/data/localhost.pid).


에러 메시지가 나온다.

지난번에는 그냥 APM 을 다시 설치했는데 이건 아닌거 같아서 구글링을 해보니 여러가지 해결책이 나오는데 대부분 해결이 안된다.


운영환경 : CentOS 6.6, MySQL 5.6.30


# cd /usr/local/mysql/data 디렉토리로 이동해서 확인해보니 ....

localhost.pid 파일이 정상동작할 때는 있다. 에러메시지가 나올 때는 이 파일이 없더라.




ps -aux | grep mysql 를 한다음 kill -9 PID 를 하라고 나오는데 mysql 프로세스가 동작되는게 없어서 안된다.


https://manage.accuwebhosting.com/knowledgebase/2342/How-to-Fix-MySQL-Error-The-server-quit-without-updating-PID-file.html


나온 7번째를 해보고 나서 정상적으로 MySQL을 Start 할 수 있었다.


Move the log file named ib_logfile in /var/lib/mysql and restart MySQL service.
cd /var/lib/mysql
mv ib_logfile0 ib_logfile0.bak
mv ib_logfile1 ib_logfile1.bak
mv /var/lib/mysql/ib_logfile* /some/tmp/folder/



ib_logfile0 ib_logfile0, ib_logfile0 ib_logfile1 두개의 파일을 다른 폴더로 이동시켰다.

이 파일은 /usr/local/mysql/data 에도 존재한다. 여기에 있는 것은 그대로 두었다.


정상동작시키고 나서 해당 파일이 자동 생성되는지 확인해봤는데 생성된 것은 없다.


정상동작하는 다른 서버에서 에러 발생을 대비하여 확인해보니....

ib_logfile0 과 ib_logfile1 은 /usr/local/mysql/data 에만 존재한다.

cd /var/lib/mysql 폴더는 아예 없다. 에러가 발생하면서 여기에 복사가 된 것인지 여부는 아직 잘 모르겠다.



openfire 메신저 파일을 설치하면서 또 한번 동일한 증상이 발생했다.

다른 폴더(/usr/local/mysql/data)에 있던 ib_logfile0, ib_logfile1 파일을 다른 폴더로 이동시키고 나서 MySQL을 재기동 시켰더니 다시 생성되더라. 그러면서 정상 동작된다.


그 이후의 증상.....(2018.11.4일 수정)

에러가 너무 자주 발생한다.

아무래도 APM을 재설치를 해줘야 할 거 같다.

블로그 이미지

Link2Me

,
728x90

CentOS 6.6 에서 Node.js 환경을 구성하는 법은 http://link2me.tistory.com/1344 게시글 참조하면 된다.


웹소켓(Web Socket)은 웹 서버와 웹 브라우저 간의 양방향 통신을 위한 프로토콜이다.
웹 소켓은 클라이언트와 서버의 연결을 항상 유지한다.
연결이 유지된 상태에서 서버 혹은 클라이언트상의 event(이벤트)가 발생하면 event listener에 의해 서버에서 클라이언트로, 또은 클라이언트에서 서버로 데이터의 전달이 이루어진다.
Node.js에서 web socket를 사용하기 위해 socket.io package를 사용한다.

동영상 강좌 등을 보면 Express Generator  를 이용하여 앱 개발하는 방법에 대해 설명을 주로 하고 있다.

http, path, fs, url 등의 모듈은 Node.js에서 기본 제공하는 패키지였기 때문에 설치할 필요가 없지만 Express는 Node.js가 아닌 다른 개인이나 단체가 만든 패키지이기 때문에 npm(node package manager)에서 다운로드 받아야 한다.


먼저 리눅스 쉘 상에서 npm install express-generator -g 를 입력하여 express-generator 을 설치한다.


# npm install express-generator -g

# express -h // 설치 여부 및 도움말 확인

설치하고 싶은 디렉토리에서 다음을 입력하여 디렉토리를 생성한다.

# express www

# cd www && npm install  // 의존성 패키지 설치


이제 socket.io 모듈을 설치한다.

# cd /home/nodejs/www

# npm install socket.io --save

# npm install cors --save

# npm install jquery --save


를 하면 package.json 파일에 버전 정보가 추가된다.




기본적인 채팅 창 구현 예제는 https://www.tutorialbook.co.kr/entry/NodeJS-Express-%EC%99%80-Socketio-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%B1%84%ED%8C%85-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0?category=587311 사이트에 설명이 잘 되어 있어 참조했다.

단, 그대로 실행하면 에러가 발생한다. 버전 정보를 맞게 수정해줘야 한다.


routers/ 디렉토리에 chat.js 파일을 생성하고 아래 코드를 추가한다.

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
    res.render('chat', { title: 'chat function' }); // View 에 chat.jade 를 추가해야됨
});
module.exports = router; 


views 디렉토리에 chat.jade 파일을 생성하고 아래 코드를 추가한다.

extends layout
 
block content
    ul#messages
    form(action='')
      input#m(autocomplete='off')
      button Send
     
    style.
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { padding:0px; font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
 
       
    script(src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js')
    script(src='http://code.jquery.com/jquery-3.3.1.js')
    script.
       
      var socket = io();
       
      $('form').submit(function(){

        // socket.emit('서버로 보낼 이벤트명', 데이터);

        socket.emit('chat message', $('#m').val());
        $('#m').val('');
        return false;
      });
     

      // socket.on('서버에서 받을 이벤트명', function(데이터)
      socket.on('chat message', function(msg){
        $('#messages').append($('<li>').text(msg));
      });
</li>


클라이언트는 socket.on과 socket.emit만 있으면 된다.

위에 색상 표시된 부분은 반드시 package.json 파일에 나온 버전 정보를 확인한 다음에 수정해줘야 정상 동작한다.

즉 설치된 npm 버전과 다르면 동작되지 않는다는 걸 명심하자.

https://cdnjs.com/libraries/socket.io 에서 소켓 관련 버전이 나온 URL를 확인할 수 있다.


 {
  "name": "www",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "^1.4.3",
    "cors": "^2.8.4",
    "debug": "~2.6.9",
    "ejs": "^2.6.1",
    "express": "~4.16.0",
    "express-session": "^1.15.6",
    "http-errors": "~1.6.2",
    "jade": "~1.11.0",
    "jquery": "^3.3.1",
    "morgan": "~1.9.0",
    "multer": "^1.3.1",
    "serve-favicon": "^2.5.0",
    "socket.io": "^2.1.1"
  }
}


app.js 파일 수정내용

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var chat = require('./routes/chat'); // 추가

var app = express();
var http = require('http').Server(app); // 추가
var io = require('socket.io')(http); // 추가

// view engine 설정
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade'); // ejs, jade

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.set( "ipaddr", "127.0.0.1" ); // 추가
app.set( "port", 3000 ); // 추가


/*** Routing ***/
app.use('/', routes);
app.use('/users', users);
app.use('/chat', chat); // 추가

http.listen(app.get('port'), function(){
    console.log("Express server listening on port " + app.get('port'));
}); // 추가

// socket.IO : Client 가 연결했을 때의 이벤트 처리
// io는 socket.io 패키지를 import한 변수
// socket은 커넥션이 성공했을 때 커넥션에 대한 정보를 담고 있는 변수
io.on('connection', function(socket){
    console.log('a user connected');
    socket.broadcast.emit('hi'); // 나를 제외한 모든 클라이언트에게 전송

    socket.on('disconnect', function(){
        // disconnect 이벤트는 클라이언트와의 연결이 끊어졌을 때 발생
        console.log('user disconnected');
    });

    socket.on('chat message', function(msg){
        console.log('message: ' + msg);
        io.sockets.emit('chat message', msg); // 나를 포함한 모든 클라이언트에게 전송
        // io.to(socket.id).emit('이벤트명' 데이터); // 특정인 한사람에게 메시지 전송
        // socket.join(방의 아이디); // 그룹에 들어가기
        // socket.leave(방의 아이디); // 그룹 떠나기
        // io.to(방의 아이디).emit('이벤트명', 데이터); // 그룹 전체 메시지 전송
        // socket.broadcast.to(방의 아이디).emit('이벤트명', 데이터); // 나를 제외한 그룹 전체 메시지 전송
        // 그룹의 목록과 그룹 안의 소켓들을 확인하는 방법
        // io.adapter.rooms
        // io.of(네임스페이스).adapter.rooms
        // socket.adapter.rooms
    });

});


// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
  app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
      message: err.message,
      error: err
    });
  });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: {}
  });
});

module.exports = app;



이제 실행을 하면 될 것이다.

#node app.js


클라이언트에서는

http://xxx.com:3000/chat

이라고 입력하면 입력할 수 있는 화면 창이 뜰 것이다.


웹 화면에서 데이터를 입력하면 접속된 모든 사용자에게 메시지가 전달되므로 본인 화면도 같이 업데이트되어 보이는 걸 확인할 수 있다.


리눅스 서버 상에서 확인하는 메시지 창

으로 user 가 접속된 걸 확인할 수 있다.

하지만 어떤 user가 접속했는지 여부는 알 수 없다.


어떤 user 가 접속했는지 여부를 알 수 있도록 하려면 회원정보와 연동시켜야 한다.

블로그 이미지

Link2Me

,
728x90

안드로이드 채팅 기능을 구현해보고 싶어 구글링 해보니 Firebase 기반으로 만든 채팅 앱 예제가 있다.

하지만 Firebase 기반은 구글 서버에 저장되므로 보안성을 요구하는 곳에서는 사용할 수 없을 거 같다.

그래서 호스팅 서버에서 구현할 수 있는 걸 찾아보니 nodejs 와 socket.io 를 이용하면 좋을 거 같아서 공부중이다.

동영상 강좌는 생활코딩 node.js https://opentutorials.org/course/2136 와 Inflearn 사이트에 있는 https://www.inflearn.com/course/node-js-%eb%85%b8%eb%93%9c%ec%a0%9c%ec%9d%b4%ec%97%90%ec%8a%a4-%ea%b0%95%ec%a2%8c/ 를 듣고 있는 중이다.

Do It Node.js 프로그래밍 책을 보고 있는데 네이버지식인 자료중에 <Node.js 프로그래밍: O'reilly / 한빛출판사> 책을 꼭 보라고 권장하고 있어서 node.js 를 깊게 공부하게 된다면 이 책도 구입 대상이다.

하지만 그럴 일이 있을지 모르겠다. Web 개발은 PHP 로 충분할 거 같고, PHP도 morden PHP 프레임웍을 배워야 하고...


Inflearn 사이트에서 하는 강좌는 WebStorm 툴 기반으로 연습해 볼 수 있는 방법으로 강좌를 진행한다.

WebStorm 툴은 Android Studio 툴을 만든 회사에서 만든 툴이라 그런지 사용방법이 매우 유사하고 편하다.

WebStorm 은 유료 툴로 1년 사용료가 129 달러라고 나온다. 30일 버전으로 사용해보고 있는 중이다.

윈도우 기반에서 편리하게 node.js 동작되는 걸 확인해보는 툴은 무료 툴도 있으니 체험기간동안만 이용해 보련다.

내가 강좌를 듣고 배워서 운영체제는 리눅스 환경이라 express 설치하는 방법이 별로 도움되지 않더라.



Express : Node.js를 위한 빠르고 간결한 개방 웹 프레임워크


http://expressjs.com/ko/ 에 설치방법이 나온다.


1. 새로운 폴더를 생성한다.
   /home/nodejs 를 폴더를 생성했다.
   express 를 테스트할 폴더를 /home/nodejs/express_app를 생성한다.


2. 생성한 폴더로 이동하여 npm init 명령을 이용하여 애플리케이션에 대한 package.json 파일을 작성한다.
   cd /home/nodejs/express_app
   npm init
   entry point: (index.js) app.js 로 적어줬다.


3. npm install express --save
   현재 폴더 하단에 node_modules 폴더가 생성되고 관련 패키지 파일이 생성된다.

   ※ node.js 파일 설치는 /usr/local/nodejs 라는 폴더에 설치하고 설정을 한 상태라서 현재 설치되는 폴더와는 다르다.


4. app.js 파일을 생성한다.
   http://expressjs.com/ko/starter/hello-world.html 에 나온 예제를 그대로 복사하여 붙여넣기 해본다.

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});


5. node app.js 를 하여 실행하고 나서
   클라이언트 웹 브라우저에서 http://weburl:3000 으로 접속을 시도하면 화면에 Hello world! 를 보여주면
   기본적인 준비가 된 상태다.


6. 추가 설치 사항

   npm install express-error-handler --save
   npm install cookie-parser --save
   npm install express-session --save
   npm install multer --save

   npm install ws --save

   npm install socket.io --save

   npm install jquery --save


Express는 HTTP 메소드에 해당하는 다음과 같은 라우팅 메소드를 지원한다.
get, post, put, head, delete, options, trace, copy, lock, mkcol, move, purge, propfind, proppatch, unlock, report, mkactivity, checkout, merge, m-search, notify, subscribe, unsubscribe, patch, search 및 connect.

블로그 이미지

Link2Me

,
728x90

안드로이드 Fragment 예제를 간략하게 정리했다.

API Level 11(Android 3.0) 이상에서 지원한다. 요즈음에는 최소 4.4 이상으로 컴파일을 하므로 기본적으로 제공된다고 볼 수 있다.


Fragment를 사용하는 가장 큰 목적은 분할된 화면들을 독립적으로 구성하고 그 상태를 관리하는데 있다.

Fragment는 항상 Activity 위에 올라가 있어야 한다.

Activity 로 만들어진 화면을 분할하여 각각의 부분화면을 Fragment로 만들고 그 Fragment를 독립적으로 관리하는 것을 목표로 한다.


Fragment는 항상 Activity 내에 포함되어 있어야 하며 해당 Fragment의 수명 주기는 호스트 Activity의 수명 주기에 직접적으로 영향을 받는다.
예를 들어 Activity가 일시정지되는 경우, 그 안의 모든 Fragment도 일시정지되며 Activity가 소멸되면 모든 Fragment도 마찬가지로 소멸된다. 그러나 Activity가 실행 중인 동안에는 각 Fragment를 추가 또는 제거하는 등 개별적으로 조작할 수 있다.


Activity 의 UI를 보여주기 위해서 onCreate 메소드를 Override 하고 Layout Resource ID인 R.layout.activity_main을 파라미터로 넘기면서 setContentView 메소드를 호출한다.


먼저, MainActivity 에 표시될 Layout XML 구조는 아래에 버튼을 클릭하면 Fragment 화면이 변경되도록 구성했다.


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

    <LinearLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:layout_above="@+id/tabmenuLayout" />

    <LinearLayout
        android:id="@+id/tabmenuLayout"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:orientation="horizontal"
        android:layout_alignParentBottom="true" >

        <Button
            android:id="@+id/btn_tab1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Tab 1"/>

        <Button
            android:id="@+id/btn_tab2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Tab 2"/>

        <Button
            android:id="@+id/btn_tab3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Tab 3"/>

    </LinearLayout>
</RelativeLayout>


fragment_container 에 표시될 Fragment XML 은 다음과 같다.

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

    <TextView
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="프래그먼트 1" />

</RelativeLayout>

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffe4e1">

    <TextView
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="프래그먼트 2" />

</RelativeLayout>

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

    <TextView
        android:id="@+id/fr_tv03"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:layout_centerInParent="true"
        android:text="프래그먼트 3"
        android:textStyle="bold"/>

</RelativeLayout>


Fragment01.java

Alt + Insert 키를 눌러서 아래 메소드를 추가한 다음에 view를 return해 주도록 코드를 수정한다.

Fragment는 Fragment Class 를 상속하여 만들 수 있다.

 public class Fragment01 extends Fragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // onCreateView() : 프래그먼트와 연관된 뷰 계층을 생성하기 위해 호출됨
        // Inflate the layout for this fragment
        ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.fragment_01, container, false);
        return rootView;
        //return super.onCreateView(inflater, container, savedInstanceState); // Alt + Insert키 누르면 자동 생성되는 return
    }
}


Fragment02.java

- Activity 에서 전달한 데이터를 받아서 화면에 표시하기

- Fragment 로 데이터 전달법은 구글에서 Passing data to activity and fragment in Android 로 검색하면 된다.

- onCreateView 에서 Activity 에서 전달한 데이터를 받아도 된다.

public class Fragment02 extends Fragment {
    TextView textView;
    String strtext;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            strtext = getArguments().getString("edttext"); // Activity 에서 받은 데이터
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.fragment_02, container, false);
        textView = (TextView) rootView.findViewById(R.id.fr_tv02);
        textView.setText(strtext);
        return rootView;
        //return super.onCreateView(inflater, container, savedInstanceState);
    }
}


Fragment03.java

 public class Fragment03 extends Fragment {
    TextView textView;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // onCreateView()로 전달된 container 매개변수가 상위 ViewGroup 이며(액티비티의 레이아웃으로부터), 이 안에 프래그먼트 레이아웃이 삽입
        // savedInstanceState 매개변수는 Bundle이며, 프래그먼트가 재개되는 중에 프래그먼트의 이전 인스턴스에 대한 데이터를 제공
        // Inflate the layout for this fragment
        // Inflate 시키고자 하는 Layout 의 Resource ID, Inflated 된 Layout 의 상위가 될 ViewGroup
        ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.fragment_03, container, false);
        textView = (TextView) rootView.findViewById(R.id.fr_tv03);
        textView.setText("Fragment 3을 선택했네요.");
        return rootView;
    }
}


MainActivity.java

Activity에서 Fragment를 불러오는 기능을 수행

 public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Context context;
    private Button tab1, tab2, tab3;
    private Fragment fragment = null;

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

        tab1 = findViewById(R.id.btn_tab1);
        tab2 = findViewById(R.id.btn_tab2);
        tab3 = findViewById(R.id.btn_tab3);

        tab1.setOnClickListener(this);
        tab2.setOnClickListener(this);
        tab3.setOnClickListener(this);

        if(findViewById(R.id.fragment_container) != null){
            if(savedInstanceState != null) return;
            Fragment01 fragment01 = new Fragment01();
            fragment01.setArguments(getIntent().getExtras());

            FragmentManager fragmentManager = getFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            // add() 메서드를 사용하여 프래그먼트를 추가하고, 추가할 프래그먼트와 이를 삽입할 뷰를 지정
            fragmentTransaction.add(R.id.fragment_container, fragment01);
            // FragmentTransaction을 변경하고 나면, 반드시 commit()을 호출해야 변경 내용이 적용됨
            fragmentTransaction.commit();
        }
        // 상세 설명은 https://developer.android.com/guide/components/fragments?hl=ko 참조
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_tab1:
                fragment = new Fragment01();
                changeFragment(fragment);
                break;
            case R.id.btn_tab2:
                Bundle bundle = new Bundle();
                bundle.putString("edttext", "From Activity");
                fragment = new Fragment02();
                fragment.setArguments(bundle); // Fragment 로 데이터 전달
                changeFragment(fragment);
                break;
            case R.id.btn_tab3:
                fragment = new Fragment03();
                changeFragment(fragment);
                break;
        }
    }

    private void changeFragment(Fragment fragment) {
        // 프로그래밍 방식으로 프래그먼트를 기존의 ViewGroup에 추가
        // 액티비티가 실행 중인 동안에는 언제든 액티비티 레이아웃에 프래그먼트를 추가 가능
        // 액티비티 내의 프래그먼트를 관리하려면 FragmentManager를 사용해야 함.
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.fragment_container,fragment);
        //fragmentTransaction.addToBackStack(null); // back 버튼을 눌렀을 때 동작하는 옵션.
        fragmentTransaction.commit(); // 트랜잭션을 액티비티에 적용하려면 반드시 commit()을 호출해야 함.
    }
}


Fragment는 Activity가 아니기 때문에 context를 갖지 않는다.
Fragment 구현시 context를 이용해야 할 경우 getActivity()함수를 이용한다.
getActivity().getApplicationContext();
getActivity().finish();
getActivity().getSharedPreferences("pref", Activity.MODE_PRIVATE);
getActivity().getSystemService(Context.USB_SERVICE);
getActivity().getSystemService(getActivity().CLIPBOARD_SERVICE);


테스트 파일

src.zip


사양이 낮은 폰에서 테스트를 못해봤는데 만약 동작되지 않는다면, import 수정.

http://www.androhub.com/android-pass-data-from-activity-to-fragment/ 에 나오는 import 참조하면 될 듯....


참조하면 도움되는 게시글

http://family-gram.tistory.com/60


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

ScrollView  (0) 2019.12.20
Dynamic Layouts  (0) 2019.01.06
FloatingActionButton(FAB)  (0) 2018.08.15
LinearLayout weight  (0) 2018.03.01
Android ViewFlipper(뷰플리퍼)  (0) 2017.05.02
블로그 이미지

Link2Me

,